@@ -0,0 +1,6 @@
|
||||
/.zed
|
||||
/.idea
|
||||
/.vscode
|
||||
/Cargo.lock
|
||||
/target
|
||||
/build_config
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"lib/semios_account",
|
||||
"lib/auth_method_fx",
|
||||
"daemon/accountd",
|
||||
"libexec/auth_password",
|
||||
"bin/account",
|
||||
]
|
||||
resolver = "3"
|
||||
|
||||
[workspace.dependencies]
|
||||
clap = { version = "4", default-features = false, features = ["std", "help", "derive"] }
|
||||
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "account"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
clap = { workspace = true }
|
||||
uuid = "1"
|
||||
semios_account = { path = "../../lib/semios_account" }
|
||||
libc = "0.2"
|
||||
@@ -0,0 +1,45 @@
|
||||
use crate::{client, util::uuid_by_one_user_filter};
|
||||
use anyhow::anyhow;
|
||||
use clap::Parser;
|
||||
use semios_account::{
|
||||
auth::{AuthCli, AuthenticateArgs, UserAuthMethodFlags},
|
||||
protocol::{GetAuthMethodPathArgs, GetUserAuthMethodsArgs},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
pub struct Cli {
|
||||
#[arg(long)]
|
||||
method: Option<String>,
|
||||
|
||||
#[arg(long, default_value = "CLI")]
|
||||
mode: String,
|
||||
|
||||
user: String,
|
||||
}
|
||||
|
||||
pub fn main(cli: Cli) -> anyhow::Result<()> {
|
||||
let mut client = client()?;
|
||||
let user = uuid_by_one_user_filter(&mut client, &cli.user)
|
||||
.ok_or_else(|| anyhow!("user \"{}\" not found", cli.user))?;
|
||||
let available_methods = client.get_user_auth_methods(GetUserAuthMethodsArgs { user })?;
|
||||
let main_method = available_methods
|
||||
.iter()
|
||||
.find(|x| x.flags.contains(UserAuthMethodFlags::MAIN));
|
||||
let Some(method) = cli.method.or(main_method.map(|x| x.name.clone())) else {
|
||||
return Err(anyhow!("no auth method available"));
|
||||
};
|
||||
if !available_methods.iter().any(|x| x.name == method) {
|
||||
return Err(anyhow!("unregistered auth method {method}"));
|
||||
}
|
||||
let executable = client.get_auth_method_path(GetAuthMethodPathArgs { name: method })?;
|
||||
let args = AuthCli::Authenticate(AuthenticateArgs {
|
||||
user_uuid: user,
|
||||
mode: cli.mode,
|
||||
})
|
||||
.compose();
|
||||
std::process::Command::new(executable)
|
||||
.args(args)
|
||||
.spawn()?
|
||||
.wait()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use crate::client;
|
||||
use anyhow::anyhow;
|
||||
use clap::Parser;
|
||||
use semios_account::{
|
||||
protocol::CreateUserArgs,
|
||||
record::{HostUid, SecureTag},
|
||||
wellknown,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
pub struct Cli {
|
||||
/// Don't create user home directory
|
||||
#[arg(short = 'H', long)]
|
||||
no_create_home: bool,
|
||||
|
||||
/// Specify custom home directory
|
||||
#[arg(short = 'd', long)]
|
||||
home: Option<String>,
|
||||
|
||||
/// Full name
|
||||
#[arg(short = 'N', long)]
|
||||
fullname: Option<String>,
|
||||
|
||||
/// Host UID
|
||||
#[arg(short = 'U', long)]
|
||||
host_uid: Option<HostUid>,
|
||||
|
||||
/// User description
|
||||
#[arg(short = 'D', long, default_value_t)]
|
||||
description: String,
|
||||
|
||||
/// Indicate that the user cannot be logged in
|
||||
#[arg(short = 'L', long)]
|
||||
no_login: bool,
|
||||
|
||||
/// CLI shell.
|
||||
#[arg(short, long, default_value = "/bin/sh")]
|
||||
cli_shell: String,
|
||||
|
||||
/// Join groups
|
||||
#[arg(short, long)]
|
||||
groups: Vec<String>,
|
||||
|
||||
username: String,
|
||||
}
|
||||
|
||||
pub fn main(cli: Cli) -> anyhow::Result<()> {
|
||||
let mut client = client()?;
|
||||
let home_directory = cli
|
||||
.home
|
||||
.unwrap_or_else(|| format!("/home/{}", cli.username));
|
||||
|
||||
let mut secure_tags = HashSet::new();
|
||||
if !cli.no_login {
|
||||
secure_tags.insert(SecureTag::Login);
|
||||
}
|
||||
|
||||
let mut groups = Vec::new();
|
||||
for i in cli.groups {
|
||||
let group_uuid = crate::util::uuid_by_one_group_filter(&mut client, &i)
|
||||
.ok_or_else(|| anyhow!("no such group \"{i}\""))?;
|
||||
groups.push(group_uuid);
|
||||
}
|
||||
|
||||
let mut defaults = HashMap::new();
|
||||
defaults.insert(wellknown::CLI_LOGIN_SHELL.into(), cli.cli_shell);
|
||||
|
||||
client.create_user(CreateUserArgs {
|
||||
username: cli.username,
|
||||
fullname: cli.fullname,
|
||||
host_uid: cli.host_uid,
|
||||
description: cli.description,
|
||||
secure_tags,
|
||||
extra_records: Default::default(),
|
||||
defaults,
|
||||
home_directory: Some(home_directory),
|
||||
groups,
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
use crate::{client, util::ManifestPrinter};
|
||||
use anyhow::anyhow;
|
||||
use clap::Parser;
|
||||
use semios_account::{protocol::GetUserInfoArgs, record::UserInfo, wellknown::CLI_LOGIN_SHELL};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
pub struct Cli {
|
||||
filter: String,
|
||||
}
|
||||
|
||||
pub fn main(cli: Cli) -> anyhow::Result<()> {
|
||||
let mut client = client()?;
|
||||
let mut found = false;
|
||||
|
||||
let mut user_tries = Vec::with_capacity(3);
|
||||
user_tries.push(GetUserInfoArgs::Username(cli.filter.clone()));
|
||||
if let Ok(uuid) = cli.filter.parse::<Uuid>() {
|
||||
user_tries.push(GetUserInfoArgs::Uuid(uuid));
|
||||
}
|
||||
|
||||
for user_try in user_tries {
|
||||
if let Ok(user_info) = client.get_user_info(user_try) {
|
||||
found = true;
|
||||
print_user_info(user_info);
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("no such user"))
|
||||
}
|
||||
}
|
||||
|
||||
fn print_user_info(user_info: UserInfo) {
|
||||
let mut mp = ManifestPrinter::new();
|
||||
|
||||
mp.add("UUID", &user_info.uuid);
|
||||
mp.add("Username", &user_info.username);
|
||||
mp.add("Full Name", &user_info.fullname);
|
||||
mp.add_optional("Host UID", &user_info.host_uid);
|
||||
mp.add_conditional("Description", &user_info.description, |x| !x.is_empty());
|
||||
mp.add("Creation Time", &user_info.creation_time);
|
||||
mp.add_conditional("Last Login Time", &user_info.last_login_time, |x| *x != 0);
|
||||
if let Some(cli_shell) = user_info.defaults.get(CLI_LOGIN_SHELL) {
|
||||
mp.add("CLI Login Shell", cli_shell);
|
||||
}
|
||||
mp.add_optional("Home Directory", &user_info.home_directory);
|
||||
|
||||
mp.finish();
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
mod authenticate;
|
||||
mod create_user;
|
||||
mod info;
|
||||
mod update_auth;
|
||||
mod util;
|
||||
|
||||
use clap::Parser;
|
||||
use semios_account::{client::Client, protocol::DEFAULT_URI};
|
||||
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
pub enum Cli {
|
||||
/// Query user or group information
|
||||
Info(info::Cli),
|
||||
|
||||
/// Create new user
|
||||
CreateUser(create_user::Cli),
|
||||
|
||||
/// Authenticate user
|
||||
Authenticate(authenticate::Cli),
|
||||
|
||||
/// Update user authentication
|
||||
UpdateAuth(update_auth::Cli),
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
let result = match cli {
|
||||
Cli::Info(cli) => info::main(cli),
|
||||
Cli::CreateUser(cli) => create_user::main(cli),
|
||||
Cli::Authenticate(cli) => authenticate::main(cli),
|
||||
Cli::UpdateAuth(cli) => update_auth::main(cli),
|
||||
};
|
||||
|
||||
if let Err(err) = result {
|
||||
eprintln!("error: {err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn is_suid_mode() -> bool {
|
||||
unsafe { libc::getuid() != libc::geteuid() && libc::geteuid() == 0 }
|
||||
}
|
||||
|
||||
fn client() -> std::io::Result<Client> {
|
||||
// If we are in SUID mode, the default uri is always used, to avoid unexpected setuid.
|
||||
if is_suid_mode() {
|
||||
Client::connect(DEFAULT_URI)
|
||||
} else {
|
||||
Client::connect_default()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
use crate::{client, util::uuid_by_one_user_filter};
|
||||
use anyhow::anyhow;
|
||||
use clap::Parser;
|
||||
use semios_account::{
|
||||
auth::{AuthCli, UpdateArgs, UserAuthMethodFlags},
|
||||
protocol::{GetAuthMethodPathArgs, GetUserAuthMethodsArgs, UserAddAuthMethodArgs},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
pub struct Cli {
|
||||
#[arg(long)]
|
||||
method: Option<String>,
|
||||
|
||||
#[arg(long, default_value = "CLI")]
|
||||
mode: String,
|
||||
|
||||
user: String,
|
||||
}
|
||||
|
||||
pub fn main(cli: Cli) -> anyhow::Result<()> {
|
||||
let mut client = client()?;
|
||||
let user = uuid_by_one_user_filter(&mut client, &cli.user)
|
||||
.ok_or_else(|| anyhow!("user \"{}\" not found", cli.user))?;
|
||||
let available_methods = client.get_user_auth_methods(GetUserAuthMethodsArgs { user })?;
|
||||
let main_method = available_methods
|
||||
.iter()
|
||||
.find(|x| x.flags.contains(UserAuthMethodFlags::MAIN));
|
||||
let Some(method) = cli.method.or(main_method.map(|x| x.name.clone())) else {
|
||||
return Err(anyhow!("no auth method available"));
|
||||
};
|
||||
if !available_methods.iter().any(|x| x.name == method) {
|
||||
client.user_add_auth_method(UserAddAuthMethodArgs {
|
||||
user,
|
||||
auth_method: method.clone(),
|
||||
})?;
|
||||
}
|
||||
let executable = client.get_auth_method_path(GetAuthMethodPathArgs { name: method })?;
|
||||
let args = AuthCli::Update(UpdateArgs {
|
||||
user_uuid: user,
|
||||
mode: cli.mode,
|
||||
})
|
||||
.compose();
|
||||
std::process::Command::new(executable)
|
||||
.args(args)
|
||||
.spawn()?
|
||||
.wait()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
use semios_account::{
|
||||
client::Client,
|
||||
protocol::{GetGroupInfoArgs, GetUserInfoArgs},
|
||||
record::HostUid,
|
||||
};
|
||||
use std::fmt::Display;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ManifestPrinter<'a>(Vec<(String, &'a dyn Display)>);
|
||||
impl<'a> ManifestPrinter<'a> {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn add(&mut self, k: impl Into<String>, v: &'a impl Display) {
|
||||
self.0.push((k.into(), v));
|
||||
}
|
||||
|
||||
pub fn add_optional(&mut self, k: impl Into<String>, v: &'a Option<impl Display>) {
|
||||
if let Some(v) = v {
|
||||
self.0.push((k.into(), v));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_conditional<V: Display>(
|
||||
&mut self,
|
||||
k: impl Into<String>,
|
||||
v: &'a V,
|
||||
c: impl FnOnce(&V) -> bool,
|
||||
) {
|
||||
if c(v) {
|
||||
self.0.push((k.into(), v));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish(self) {
|
||||
let max_key_len = self.0.iter().map(|x| x.0.len()).max().unwrap_or_default();
|
||||
let width = max_key_len + 2;
|
||||
for (k, v) in self.0 {
|
||||
println!("{:width$}: {}", k, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn uuid_by_one_user_filter(client: &mut Client, filter: &str) -> Option<Uuid> {
|
||||
if let Ok(uuid) = filter.parse::<Uuid>() {
|
||||
return Some(uuid);
|
||||
}
|
||||
|
||||
let args = match filter.parse::<HostUid>() {
|
||||
Ok(uid) => GetUserInfoArgs::HostUid(uid),
|
||||
Err(_) => GetUserInfoArgs::Username(filter.into()),
|
||||
};
|
||||
|
||||
Some(client.get_user_info(args).ok()?.uuid)
|
||||
}
|
||||
|
||||
pub fn uuid_by_one_group_filter(client: &mut Client, filter: &str) -> Option<Uuid> {
|
||||
if let Ok(uuid) = filter.parse::<Uuid>() {
|
||||
return Some(uuid);
|
||||
}
|
||||
|
||||
let args = match filter.parse::<HostUid>() {
|
||||
Ok(uid) => GetGroupInfoArgs::HostGid(uid),
|
||||
Err(_) => GetGroupInfoArgs::Groupname(filter.into()),
|
||||
};
|
||||
|
||||
Some(client.get_group_info(args).ok()?.uuid)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "accountd"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
airup = []
|
||||
embed_init_file = []
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
cfg-if = "1"
|
||||
bitflags = "2"
|
||||
clap = { workspace = true }
|
||||
rusqlite = "0.39"
|
||||
rustc-hash = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
semios_account = { path = "../../lib/semios_account" }
|
||||
uuid = { version = "1", features = ["v7"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1"
|
||||
features = ["rt", "net", "macros", "io-util", "process", "signal", "sync", "time"]
|
||||
@@ -0,0 +1 @@
|
||||
// Sandbox rules for running an authentication method.
|
||||
@@ -0,0 +1,387 @@
|
||||
//! Implementation of the API server.
|
||||
|
||||
use crate::{
|
||||
AppState,
|
||||
util::{
|
||||
ipc::{Connection, Listener},
|
||||
time::timestamp_s,
|
||||
},
|
||||
};
|
||||
use rustc_hash::FxHashMap;
|
||||
use semios_account::{
|
||||
auth::{UserAuthMethodFlags, UserAuthMethodRecord},
|
||||
error::Error,
|
||||
protocol::*,
|
||||
record::{GroupInfo, SecureTag, UserInfo},
|
||||
secret::Secret,
|
||||
};
|
||||
use std::{fmt::Debug, path::PathBuf, pin::Pin, sync::Arc};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub async fn launch_server(app_state: Arc<AppState>) -> anyhow::Result<()> {
|
||||
let context = Arc::new(ApiContext::new(app_state));
|
||||
let listener = Listener::new(&semios_account::protocol::uri()).await?;
|
||||
let server = Server { context, listener };
|
||||
tokio::spawn(server.run());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
macro_rules! compose {
|
||||
($orig:ident) => {
|
||||
Box::new(|state, caller, args| {
|
||||
Box::pin(async move {
|
||||
let args = match serde_json::from_value(args) {
|
||||
Ok(x) => x,
|
||||
Err(e) => return Err(Error::InvalidParams(e.to_string())),
|
||||
};
|
||||
$orig(state, caller, args)
|
||||
.await
|
||||
.map(|x| serde_json::to_value(x).expect("invalid api return value"))
|
||||
}) as FutureMethod
|
||||
}) as FnMethod
|
||||
};
|
||||
}
|
||||
|
||||
type FutureMethod = Pin<Box<dyn Future<Output = Result<serde_json::Value, Error>> + Send>>;
|
||||
type FnMethod = Box<dyn Fn(Arc<AppState>, Caller, serde_json::Value) -> FutureMethod + Send + Sync>;
|
||||
|
||||
/// A context for API users.
|
||||
pub struct ApiContext {
|
||||
methods: FxHashMap<&'static str, FnMethod>,
|
||||
app_state: Arc<AppState>,
|
||||
}
|
||||
impl ApiContext {
|
||||
pub fn new(app_state: Arc<AppState>) -> Self {
|
||||
let mut methods = FxHashMap::default();
|
||||
methods.insert(GET_USER_INFO, compose!(get_user_info));
|
||||
methods.insert(GET_GROUP_INFO, compose!(get_group_info));
|
||||
methods.insert(LIST_USER, compose!(list_user));
|
||||
methods.insert(LIST_GROUP, compose!(list_group));
|
||||
methods.insert(CREATE_USER, compose!(create_user));
|
||||
methods.insert(REMOVE_USER, compose!(remove_user));
|
||||
methods.insert(CREATE_GROUP, compose!(create_group));
|
||||
methods.insert(GET_SECRET, compose!(get_secret));
|
||||
methods.insert(SET_SECRET, compose!(set_secret));
|
||||
methods.insert(GET_USER_AUTH_METHODS, compose!(get_user_auth_methods));
|
||||
methods.insert(USER_ADD_AUTH_METHOD, compose!(user_add_auth_method));
|
||||
methods.insert(GET_AUTH_METHOD_PATH, compose!(get_auth_method_path));
|
||||
Self { methods, app_state }
|
||||
}
|
||||
|
||||
pub async fn invoke(&self, method: &str, caller: Caller, args: serde_json::Value) -> Response {
|
||||
let Some(method) = self.methods.get(&method) else {
|
||||
return Response::from_result::<()>(Err(Error::NotImplemented(method.into())));
|
||||
};
|
||||
Response::from_result(method(self.app_state.clone(), caller, args).await)
|
||||
}
|
||||
}
|
||||
impl Debug for ApiContext {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ApiContext")
|
||||
.field("methods", &self.methods.keys())
|
||||
.field("app_state", &self.app_state)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Caller {
|
||||
/// Unknown user. No privileges are granted.
|
||||
Unknown,
|
||||
|
||||
/// Explicit user.
|
||||
User(Uuid),
|
||||
|
||||
/// All privileges are granted, but no user is specified.
|
||||
Immortal,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Server {
|
||||
context: Arc<ApiContext>,
|
||||
listener: Listener,
|
||||
}
|
||||
impl Server {
|
||||
pub async fn run(self) -> anyhow::Result<()> {
|
||||
loop {
|
||||
let connection = match self.listener.accept().await {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
tracing::warn!("ipc_accept() failed: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let session = Session {
|
||||
context: self.context.clone(),
|
||||
connection,
|
||||
caller: Caller::Unknown,
|
||||
};
|
||||
tokio::spawn(session.run());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Session {
|
||||
context: Arc<ApiContext>,
|
||||
connection: Connection,
|
||||
caller: Caller,
|
||||
}
|
||||
impl Session {
|
||||
pub async fn run(mut self) -> anyhow::Result<()> {
|
||||
let peer_uuid = match self.connection.peer_host_uid() {
|
||||
Some(uid) => self.context.app_state.local_db.find_user_by_host_uid(uid),
|
||||
None => None,
|
||||
};
|
||||
self.caller = match peer_uuid {
|
||||
Some(uuid) => Caller::User(uuid),
|
||||
None => Caller::Unknown,
|
||||
};
|
||||
let mut buf = Vec::with_capacity(1024);
|
||||
loop {
|
||||
self.connection.recv(&mut buf).await?;
|
||||
let req: Request = serde_json::from_slice(&buf)?;
|
||||
let resp = self
|
||||
.context
|
||||
.invoke(&req.method, self.caller, req.params)
|
||||
.await;
|
||||
buf.clear();
|
||||
serde_json::to_writer(&mut buf, &resp).expect("invalid response emitted");
|
||||
self.connection.send(&buf).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_user(
|
||||
state: Arc<AppState>,
|
||||
caller: Caller,
|
||||
args: ListUserArgs,
|
||||
) -> Result<Vec<Uuid>, Error> {
|
||||
Ok(state.local_db.list_user(args.start, args.len))
|
||||
}
|
||||
|
||||
async fn list_group(
|
||||
state: Arc<AppState>,
|
||||
caller: Caller,
|
||||
args: ListGroupArgs,
|
||||
) -> Result<Vec<Uuid>, Error> {
|
||||
Ok(state.local_db.list_group(args.start, args.len))
|
||||
}
|
||||
|
||||
async fn get_user_info(
|
||||
state: Arc<AppState>,
|
||||
caller: Caller,
|
||||
args: GetUserInfoArgs,
|
||||
) -> Result<UserInfo, Error> {
|
||||
let uuid = match args {
|
||||
GetUserInfoArgs::Uuid(x) => x,
|
||||
GetUserInfoArgs::HostUid(uid) => state
|
||||
.local_db
|
||||
.find_user_by_host_uid(uid)
|
||||
.ok_or(Error::NoSuchUser)?,
|
||||
GetUserInfoArgs::Username(name) => state
|
||||
.local_db
|
||||
.find_user_by_name(&name)
|
||||
.ok_or(Error::NoSuchUser)?,
|
||||
};
|
||||
state.local_db.get_user_info(&uuid).ok_or(Error::NoSuchUser)
|
||||
}
|
||||
|
||||
async fn get_group_info(
|
||||
state: Arc<AppState>,
|
||||
caller: Caller,
|
||||
args: GetGroupInfoArgs,
|
||||
) -> Result<GroupInfo, Error> {
|
||||
let uuid = match args {
|
||||
GetGroupInfoArgs::Uuid(x) => x,
|
||||
GetGroupInfoArgs::HostGid(gid) => state
|
||||
.local_db
|
||||
.find_group_by_host_gid(gid)
|
||||
.ok_or(Error::NoSuchGroup)?,
|
||||
GetGroupInfoArgs::Groupname(name) => state
|
||||
.local_db
|
||||
.find_group_by_name(&name)
|
||||
.ok_or(Error::NoSuchGroup)?,
|
||||
};
|
||||
state
|
||||
.local_db
|
||||
.get_group_info(&uuid)
|
||||
.ok_or(Error::NoSuchGroup)
|
||||
}
|
||||
|
||||
async fn create_user(
|
||||
state: Arc<AppState>,
|
||||
caller: Caller,
|
||||
args: CreateUserArgs,
|
||||
) -> Result<(), Error> {
|
||||
require_secure_tags(&state, caller, &[SecureTag::CreateUser]).await?;
|
||||
let user_info = UserInfo {
|
||||
uuid: Uuid::new_v7(uuid::Timestamp::now(uuid::ContextV7::new())),
|
||||
fullname: args.fullname.unwrap_or_else(|| args.username.clone()),
|
||||
username: args.username,
|
||||
host_uid: args.host_uid,
|
||||
description: args.description,
|
||||
secure_tags: args.secure_tags,
|
||||
extra_records: args.extra_records,
|
||||
defaults: args.defaults,
|
||||
home_directory: args.home_directory,
|
||||
creation_time: timestamp_s(),
|
||||
last_login_time: 0,
|
||||
};
|
||||
state
|
||||
.local_db
|
||||
.insert_user_info(user_info)
|
||||
.map_err(|_| Error::AlreadyExists)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_group(
|
||||
state: Arc<AppState>,
|
||||
caller: Caller,
|
||||
args: CreateGroupArgs,
|
||||
) -> Result<(), Error> {
|
||||
require_secure_tags(&state, caller, &[SecureTag::CreateUser]).await?;
|
||||
let group_info = GroupInfo {
|
||||
uuid: Uuid::new_v7(uuid::Timestamp::now(uuid::ContextV7::new())),
|
||||
fullname: args.fullname.unwrap_or_else(|| args.groupname.clone()),
|
||||
groupname: args.groupname,
|
||||
host_gid: args.host_gid,
|
||||
description: args.description,
|
||||
creation_time: timestamp_s(),
|
||||
};
|
||||
state
|
||||
.local_db
|
||||
.insert_group_info(group_info)
|
||||
.map_err(|_| Error::AlreadyExists)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_user(
|
||||
state: Arc<AppState>,
|
||||
caller: Caller,
|
||||
args: RemoveUserArgs,
|
||||
) -> Result<(), Error> {
|
||||
require_secure_tags(&state, caller, &[SecureTag::RemoveUser]).await?;
|
||||
todo!();
|
||||
}
|
||||
|
||||
async fn get_secret(
|
||||
state: Arc<AppState>,
|
||||
caller: Caller,
|
||||
args: GetSecretArgs,
|
||||
) -> Result<Secret, Error> {
|
||||
require_secure_tags(&state, caller, &[SecureTag::ReadSecret])
|
||||
.await
|
||||
.or(require_same_user(&state, caller, args.user).await)?;
|
||||
|
||||
let raw_secret = state
|
||||
.local_db
|
||||
.get_secret(&args.user, &args.name)
|
||||
.ok_or(Error::NoSuchSecret)?;
|
||||
|
||||
Ok(raw_secret)
|
||||
}
|
||||
|
||||
async fn set_secret(
|
||||
state: Arc<AppState>,
|
||||
caller: Caller,
|
||||
args: SetSecretArgs,
|
||||
) -> Result<(), Error> {
|
||||
require_secure_tags(&state, caller, &[SecureTag::WriteSecret])
|
||||
.await
|
||||
.or(require_same_user(&state, caller, args.user).await)?;
|
||||
|
||||
let raw_secret = args.secret;
|
||||
|
||||
_ = state.local_db.delete_secret(&args.user, &raw_secret.name);
|
||||
state.local_db.insert_secret(&args.user, raw_secret)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_user_auth_methods(
|
||||
state: Arc<AppState>,
|
||||
caller: Caller,
|
||||
args: GetUserAuthMethodsArgs,
|
||||
) -> Result<Vec<UserAuthMethodRecord>, Error> {
|
||||
require_secure_tags(&state, Caller::User(args.user), &[SecureTag::Login]).await?;
|
||||
state.local_db.get_user_auth_methods(&args.user)
|
||||
}
|
||||
|
||||
async fn user_add_auth_method(
|
||||
state: Arc<AppState>,
|
||||
caller: Caller,
|
||||
args: UserAddAuthMethodArgs,
|
||||
) -> Result<(), Error> {
|
||||
require_secure_tags(&state, Caller::User(args.user), &[SecureTag::Login]).await?;
|
||||
|
||||
let current = state.local_db.get_user_auth_methods(&args.user)?;
|
||||
let exec = state
|
||||
.auth_method(&args.auth_method)
|
||||
.ok_or(Error::NoSuchAuthMethod)?;
|
||||
let info = exec
|
||||
.info()
|
||||
.await
|
||||
.map_err(|e| Error::Internal(e.to_string()))?;
|
||||
let mut flags = UserAuthMethodFlags::empty();
|
||||
if info.provides_master_key && current.is_empty() {
|
||||
flags |= UserAuthMethodFlags::MAIN;
|
||||
}
|
||||
state
|
||||
.local_db
|
||||
.insert_user_auth_method(&args.user, &args.auth_method, flags)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_auth_method_path(
|
||||
state: Arc<AppState>,
|
||||
caller: Caller,
|
||||
args: GetAuthMethodPathArgs,
|
||||
) -> Result<PathBuf, Error> {
|
||||
Ok(state
|
||||
.auth_method(&args.name)
|
||||
.ok_or(Error::NoSuchAuthMethod)?
|
||||
.path()
|
||||
.into())
|
||||
}
|
||||
|
||||
// ==- Helpers -==
|
||||
|
||||
async fn require_same_user(
|
||||
state: &AppState,
|
||||
caller: Caller,
|
||||
requested_user: Uuid,
|
||||
) -> Result<(), Error> {
|
||||
let peer_user = match caller {
|
||||
Caller::Unknown => return Err(Error::PermissionDenied),
|
||||
Caller::User(uuid) => uuid,
|
||||
Caller::Immortal => return Ok(()),
|
||||
};
|
||||
if requested_user == peer_user {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::PermissionDenied)
|
||||
}
|
||||
}
|
||||
|
||||
async fn require_secure_tags(
|
||||
state: &AppState,
|
||||
caller: Caller,
|
||||
tags: &[SecureTag],
|
||||
) -> Result<(), Error> {
|
||||
let peer_user = match caller {
|
||||
Caller::Unknown => return Err(Error::PermissionDenied),
|
||||
Caller::User(uuid) => uuid,
|
||||
Caller::Immortal => return Ok(()),
|
||||
};
|
||||
let user_info = state
|
||||
.local_db
|
||||
.get_user_info(&peer_user)
|
||||
.ok_or(Error::PermissionDenied)?;
|
||||
for tag in tags {
|
||||
if !user_info.secure_tags.contains(tag) {
|
||||
return Err(Error::PermissionDenied);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
use anyhow::anyhow;
|
||||
use semios_account::auth::{AuthCli, AuthMethodInfo};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
process::Stdio,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AuthExec(PathBuf);
|
||||
impl AuthExec {
|
||||
pub fn from_path_buf(path_buf: PathBuf) -> Self {
|
||||
Self(path_buf)
|
||||
}
|
||||
|
||||
pub fn name(&self) -> anyhow::Result<String> {
|
||||
let err = || anyhow!("invalid auth exec \"{}\"", self.0.display());
|
||||
self.0
|
||||
.file_name()
|
||||
.ok_or_else(err)?
|
||||
.to_string_lossy()
|
||||
.strip_prefix("auth_")
|
||||
.ok_or_else(err)
|
||||
.map(Into::into)
|
||||
}
|
||||
|
||||
pub async fn info(&self) -> anyhow::Result<AuthMethodInfo> {
|
||||
let wait = tokio::process::Command::new(&self.0)
|
||||
.args(AuthCli::QueryInformation.compose())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()?
|
||||
.wait_with_output()
|
||||
.await?;
|
||||
|
||||
if !wait.status.success() {
|
||||
return Err(anyhow!(
|
||||
"process \"{}\" exited with status code {}",
|
||||
self.0.display(),
|
||||
wait.status
|
||||
));
|
||||
}
|
||||
let parsed: AuthMethodInfo = serde_json::from_slice(&wait.stdout)?;
|
||||
|
||||
if parsed.name != self.name()? {
|
||||
return Err(anyhow!(
|
||||
"mismatched filename \"{}\" for auth method \"{}\"",
|
||||
self.name()?,
|
||||
parsed.name
|
||||
));
|
||||
}
|
||||
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
//! Initialization on first run.
|
||||
|
||||
use crate::{
|
||||
AppState,
|
||||
api::{ApiContext, Caller},
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use semios_account::protocol::Request;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(feature = "embed_init_file")]
|
||||
const EMBEDDED: &str = include_str!("../../../build_config/init.json");
|
||||
|
||||
#[cfg(not(feature = "embed_init_file"))]
|
||||
const EMBEDDED: &str = "[]";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct InitCommand {
|
||||
pub on_failure: OnFailure,
|
||||
pub request: Request,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum OnFailure {
|
||||
Terminate,
|
||||
Warn,
|
||||
Skip,
|
||||
}
|
||||
|
||||
pub async fn run(app_state: Arc<AppState>) -> anyhow::Result<()> {
|
||||
let init_file: Vec<InitCommand> = serde_json::from_str(EMBEDDED)?;
|
||||
let api_context = ApiContext::new(app_state);
|
||||
for cmd in init_file {
|
||||
let resp = api_context
|
||||
.invoke(
|
||||
&cmd.request.method,
|
||||
Caller::Immortal,
|
||||
cmd.request.params.clone(),
|
||||
)
|
||||
.await;
|
||||
if resp.success {
|
||||
tracing::debug!(
|
||||
"Successfully ran init_command {:?} with response {:?}",
|
||||
cmd,
|
||||
resp
|
||||
);
|
||||
} else {
|
||||
match cmd.on_failure {
|
||||
OnFailure::Terminate => {
|
||||
tracing::error!("Failed to run init_command {:?}: {:?}", cmd, resp);
|
||||
return Err(anyhow!(
|
||||
"An init command failed to run, see logs for details"
|
||||
));
|
||||
}
|
||||
OnFailure::Warn => {
|
||||
tracing::warn!("Failed to run init_command {:?}: {:?}", cmd, resp);
|
||||
}
|
||||
OnFailure::Skip => {
|
||||
tracing::debug!("Failed to run init_command {:?}: {:?}", cmd, resp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
use rusqlite::params;
|
||||
use semios_account::{
|
||||
auth::{UserAuthMethodFlags, UserAuthMethodRecord},
|
||||
error::Error,
|
||||
record::{GroupInfo, HostGid, HostUid, UserInfo},
|
||||
secret::{Secret, SecurityFlags},
|
||||
};
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::{path::Path, sync::Mutex};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Schema of the database.
|
||||
///
|
||||
/// The "user" table has columns:
|
||||
/// - "user_uuid": UUID of the user.
|
||||
/// - "user_name": Name of the user.
|
||||
/// - "user_host_uid": Host UID of the user.
|
||||
/// - "user_info": User information JSON (contains host specific information).
|
||||
///
|
||||
/// The "group" table has columns:
|
||||
/// - "group_uuid": UUID of the group.
|
||||
/// - "group_name": Name of the group.
|
||||
/// - "group_host_gid": Host GID of the group.
|
||||
/// - "group_info": Group information JSON (contains host specific information).
|
||||
///
|
||||
/// The "kv_store" table is used to store automatic configurations that are not edited by user.
|
||||
///
|
||||
/// The "secret" table has columns:
|
||||
/// - "secret_owner": Owner user of the secret, in UUID text.
|
||||
/// - "secret_name": Name of the secret.
|
||||
/// - "secret_data": Data of the secret.
|
||||
/// - "secret_security_flags": Secret security flags.
|
||||
/// - "secret_creation_time": Timestamp the secret is created.
|
||||
/// - "secret_expiration_time": Timestamp the secret will be expired.
|
||||
///
|
||||
/// The "group_membership" table has columns:
|
||||
/// - "group_membership_user": UUID of the user in the record.
|
||||
/// - "group_membership_group": UUID of the group in the record.
|
||||
///
|
||||
/// The "user_auth" table has columns:
|
||||
/// - "user_auth_user": UUID of the user in the record.
|
||||
/// - "user_auth_method": Name of the auth method specified in the record.
|
||||
/// - "user_auth_flags": Flags of the record.
|
||||
const SCHEMA: &str = "
|
||||
CREATE TABLE IF NOT EXISTS \"user\"(
|
||||
\"user_uuid\" TEXT PRIMARY KEY,
|
||||
\"user_name\" TEXT NOT NULL UNIQUE,
|
||||
\"user_host_uid\" TEXT NOT NULL UNIQUE,
|
||||
\"user_info\" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS \"group\"(
|
||||
\"group_uuid\" TEXT PRIMARY KEY,
|
||||
\"group_name\" TEXT NOT NULL UNIQUE,
|
||||
\"group_host_gid\" TEXT NOT NULL UNIQUE,
|
||||
\"group_info\" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS \"kv_store\"(
|
||||
\"kv_store_key\" TEXT PRIMARY KEY,
|
||||
\"kv_store_value\" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS \"secret\"(
|
||||
\"secret_owner\" TEXT,
|
||||
\"secret_name\" TEXT,
|
||||
\"secret_data\" BLOB NOT NULL,
|
||||
\"secret_security_flags\" INTEGER NOT NULL,
|
||||
\"secret_creation_time\" INTEGER NOT NULL,
|
||||
\"secret_expiration_time\" INTEGER NOT NULL,
|
||||
PRIMARY KEY (\"secret_owner\", \"secret_name\")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS \"group_membership\"(
|
||||
\"group_membership_user\" TEXT NOT NULL,
|
||||
\"group_membership_group\" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS \"user_auth\"(
|
||||
\"user_auth_user\" TEXT NOT NULL,
|
||||
\"user_auth_method\" TEXT NOT NULL,
|
||||
\"user_auth_flags\" INTEGER NOT NULL
|
||||
);
|
||||
";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LocalDb(Mutex<rusqlite::Connection>);
|
||||
impl LocalDb {
|
||||
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
|
||||
let connection = rusqlite::Connection::open(path).map_err(Error::make_internal)?;
|
||||
connection
|
||||
.execute_batch(SCHEMA)
|
||||
.map_err(Error::make_internal)?;
|
||||
Ok(Self(Mutex::new(connection)))
|
||||
}
|
||||
|
||||
pub fn get_secret(&self, owner: &Uuid, name: &str) -> Option<Secret> {
|
||||
let lock = self.0.lock().unwrap();
|
||||
let mut stmt = lock
|
||||
.prepare_cached(
|
||||
"SELECT * FROM \"secret\" WHERE \"secret_owner\" = ?1 AND \"secret_name\" = ?2",
|
||||
)
|
||||
.ok()?;
|
||||
stmt.query_one(params![owner.to_string(), name], |row| {
|
||||
Ok(Secret {
|
||||
name: name.into(),
|
||||
data: row.get("secret_data")?,
|
||||
security_flags: SecurityFlags::from_bits_retain(row.get("secret_security_flags")?),
|
||||
creation_time: row.get("secret_creation_time")?,
|
||||
expiration_time: row.get("secret_expiration_time")?,
|
||||
})
|
||||
})
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub fn insert_secret(&self, owner: &Uuid, secret: Secret) -> Result<(), Error> {
|
||||
let mut lock = self.0.lock().unwrap();
|
||||
execute(
|
||||
&mut *lock,
|
||||
"INSERT INTO \"secret\"(
|
||||
\"secret_owner\",
|
||||
\"secret_name\",
|
||||
\"secret_data\",
|
||||
\"secret_security_flags\",
|
||||
\"secret_creation_time\",
|
||||
\"secret_expiration_time\"
|
||||
) VALUES(?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
params![
|
||||
owner.to_string(),
|
||||
secret.name,
|
||||
secret.data,
|
||||
secret.security_flags.bits(),
|
||||
secret.creation_time,
|
||||
secret.expiration_time
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn delete_secret(&self, uuid: &Uuid, name: &str) -> Result<(), Error> {
|
||||
execute(
|
||||
&mut *self.0.lock().unwrap(),
|
||||
"DELETE FROM \"secret\" WHERE \"secret_owner\" = ?1 AND \"secret_name\" = ?2",
|
||||
params![uuid.to_string(), name],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn list_user(&self, start: u32, len: u32) -> Vec<Uuid> {
|
||||
self._list_user_group("user", start, len)
|
||||
}
|
||||
|
||||
pub fn list_group(&self, start: u32, len: u32) -> Vec<Uuid> {
|
||||
self._list_user_group("group", start, len)
|
||||
}
|
||||
|
||||
pub fn get_user_info(&self, uuid: &Uuid) -> Option<UserInfo> {
|
||||
self._get_user_group_info("user", uuid)
|
||||
}
|
||||
|
||||
pub fn get_group_info(&self, uuid: &Uuid) -> Option<GroupInfo> {
|
||||
self._get_user_group_info("group", uuid)
|
||||
}
|
||||
|
||||
pub fn insert_user_info(&self, user_info: UserInfo) -> Result<(), Error> {
|
||||
let mut lock = self.0.lock().unwrap();
|
||||
execute(
|
||||
&mut *lock,
|
||||
"INSERT INTO \"user\"(\"user_uuid\", \"user_name\", \"user_host_uid\", \"user_info\")
|
||||
VALUES(?1, ?2, ?3, ?4)",
|
||||
params![
|
||||
user_info.uuid.to_string(),
|
||||
user_info.username,
|
||||
user_info
|
||||
.host_uid
|
||||
.ok_or_else(|| Error::make_internal("host_uid not specified"))?,
|
||||
serde_json::to_string(&user_info).map_err(Error::make_internal)?,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn delete_user(&self, uuid: &Uuid) -> Result<(), Error> {
|
||||
let mut lock = self.0.lock().unwrap();
|
||||
execute(
|
||||
&mut *lock,
|
||||
"DELETE FROM \"user\" WHERE \"user_uuid\" = ?1",
|
||||
params![uuid.to_string()],
|
||||
)?;
|
||||
execute(
|
||||
&mut *lock,
|
||||
"DELETE FROM \"secret\" WHERE \"secret_owner\" = ?1",
|
||||
params![uuid.to_string()],
|
||||
)?;
|
||||
execute(
|
||||
&mut *lock,
|
||||
"DELETE FROM \"group_membership\" WHERE \"group_membership_user\" = ?1",
|
||||
params![uuid.to_string()],
|
||||
)?;
|
||||
execute(
|
||||
&mut *lock,
|
||||
"DELETE FROM \"user_auth\" WHERE \"user_auth_user\" = ?1",
|
||||
params![uuid.to_string()],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn insert_group_info(&self, group_info: GroupInfo) -> Result<(), Error> {
|
||||
let mut lock = self.0.lock().unwrap();
|
||||
execute(
|
||||
&mut *lock,
|
||||
"INSERT INTO \"group\"(\"group_uuid\", \"group_name\", \"group_host_gid\", \"group_info\")
|
||||
VALUES(?1, ?2, ?3, ?4)",
|
||||
params![
|
||||
group_info.uuid.to_string(),
|
||||
group_info.groupname,
|
||||
group_info
|
||||
.host_gid
|
||||
.ok_or_else(|| Error::make_internal("host_gid not specified"))?,
|
||||
serde_json::to_string(&group_info).map_err(Error::make_internal)?,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn delete_group(&self, uuid: &Uuid) -> Result<(), Error> {
|
||||
let mut lock = self.0.lock().unwrap();
|
||||
execute(
|
||||
&mut *lock,
|
||||
"DELETE FROM \"group\" WHERE \"group_uuid\" = ?1",
|
||||
params![uuid.to_string()],
|
||||
)?;
|
||||
execute(
|
||||
&mut *lock,
|
||||
"DELETE FROM \"group_membership\" WHERE \"group_membership_group\" = ?1",
|
||||
params![uuid.to_string()],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_user_auth_methods(&self, uuid: &Uuid) -> Result<Vec<UserAuthMethodRecord>, Error> {
|
||||
let lock = self.0.lock().unwrap();
|
||||
let mut stmt = lock
|
||||
.prepare_cached("SELECT * FROM \"user_auth\" WHERE \"user_auth_user\" = ?1")
|
||||
.unwrap();
|
||||
let mut rows = stmt
|
||||
.query(params![uuid.to_string()])
|
||||
.map_err(Error::make_internal)?;
|
||||
let mut all = Vec::new();
|
||||
while let Ok(Some(i)) = rows.next() {
|
||||
let name = i.get("user_auth_method").map_err(Error::make_internal)?;
|
||||
let flags = i.get("user_auth_flags").map_err(Error::make_internal)?;
|
||||
all.push(UserAuthMethodRecord {
|
||||
name,
|
||||
flags: UserAuthMethodFlags::from_bits_retain(flags),
|
||||
});
|
||||
}
|
||||
Ok(all)
|
||||
}
|
||||
|
||||
pub fn insert_user_auth_method(
|
||||
&self,
|
||||
uuid: &Uuid,
|
||||
method: &str,
|
||||
flags: UserAuthMethodFlags,
|
||||
) -> Result<(), Error> {
|
||||
let mut lock = self.0.lock().unwrap();
|
||||
execute(
|
||||
&mut *lock,
|
||||
"INSERT INTO \"user_auth\"(\"user_auth_user\", \"user_auth_method\", \"user_auth_flags\")
|
||||
VALUES(?1, ?2, ?3)",
|
||||
params![uuid.to_string(), method, flags.bits()],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn find_user_by_name(&self, username: &str) -> Option<Uuid> {
|
||||
self._find_uuid_by("user", "user_name", &username)
|
||||
}
|
||||
|
||||
pub fn find_group_by_name(&self, groupname: &str) -> Option<Uuid> {
|
||||
self._find_uuid_by("group", "group_name", &groupname)
|
||||
}
|
||||
|
||||
pub fn find_user_by_host_uid(&self, host_uid: HostUid) -> Option<Uuid> {
|
||||
self._find_uuid_by("user", "user_host_uid", &host_uid.to_string())
|
||||
}
|
||||
|
||||
pub fn find_group_by_host_gid(&self, host_gid: HostGid) -> Option<Uuid> {
|
||||
self._find_uuid_by("group", "group_host_gid", &host_gid.to_string())
|
||||
}
|
||||
|
||||
pub fn is_first_run(&self) -> bool {
|
||||
!self._kv_get("initialized").is_some()
|
||||
}
|
||||
|
||||
pub fn unset_first_run(&self) -> Result<(), Error> {
|
||||
self._kv_set("initialized", "1")
|
||||
.map_err(Error::make_internal)
|
||||
}
|
||||
|
||||
fn _list_user_group(&self, table: &'static str, start: u32, len: u32) -> Vec<Uuid> {
|
||||
let lock = self.0.lock().unwrap();
|
||||
let stmt = lock.prepare_cached(&format!(
|
||||
"SELECT \"{table}_uuid\" FROM \"{table}\" ORDER BY \"{table}_uuid\" LIMIT ?1 OFFSET ?2"
|
||||
));
|
||||
let Ok(mut stmt) = stmt else {
|
||||
return Vec::new();
|
||||
};
|
||||
stmt.query_map(params![len, start], |x| {
|
||||
x.get::<_, String>(&format!("{table}_uuid")[..])?
|
||||
.parse::<Uuid>()
|
||||
.map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))
|
||||
})
|
||||
.map(|x| x.filter_map(|y| y.ok()).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn _get_user_group_info<T: DeserializeOwned>(
|
||||
&self,
|
||||
table: &'static str,
|
||||
uuid: &Uuid,
|
||||
) -> Option<T> {
|
||||
let lock = self.0.lock().unwrap();
|
||||
let mut stmt = lock
|
||||
.prepare_cached(&format!(
|
||||
"SELECT * FROM \"{table}\" WHERE \"{table}_uuid\" = ?1"
|
||||
))
|
||||
.ok()?;
|
||||
stmt.query_one(params![uuid.to_string()], |rows| {
|
||||
serde_json::from_str(&rows.get::<_, String>(&format!("{table}_info")[..])?)
|
||||
.map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))
|
||||
})
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn _find_uuid_by<T: rusqlite::types::ToSql>(
|
||||
&self,
|
||||
table: &'static str,
|
||||
column: &'static str,
|
||||
val: &T,
|
||||
) -> Option<Uuid> {
|
||||
let lock = self.0.lock().unwrap();
|
||||
let mut stmt = lock
|
||||
.prepare_cached(&format!(
|
||||
"SELECT \"{table}_uuid\" FROM \"{table}\" WHERE \"{column}\" = ?1"
|
||||
))
|
||||
.ok()?;
|
||||
stmt.query_one(params![val], |rows| {
|
||||
rows.get::<_, String>(&format!("{table}_uuid")[..])
|
||||
})
|
||||
.ok()?
|
||||
.parse::<Uuid>()
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn _kv_get(&self, key: &str) -> Option<String> {
|
||||
let lock = self.0.lock().unwrap();
|
||||
let mut stmt = lock
|
||||
.prepare_cached(
|
||||
"SELECT \"kv_store_value\" FROM \"kv_store\" WHERE \"kv_store_key\" = ?1",
|
||||
)
|
||||
.ok()?;
|
||||
stmt.query_one(params![key], |rows| rows.get("kv_store_value"))
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn _kv_set(&self, key: &str, val: &str) -> rusqlite::Result<()> {
|
||||
let lock = self.0.lock().unwrap();
|
||||
let mut stmt_get = lock.prepare_cached(
|
||||
"SELECT \"kv_store_key\" FROM \"kv_store\" WHERE \"kv_store_key\" = ?1",
|
||||
)?;
|
||||
let mut stmt_set = lock.prepare_cached(
|
||||
"UPDATE \"kv_store\" SET \"kv_store_value\" = ?1 WHERE \"kv_store_key\" = ?2",
|
||||
)?;
|
||||
let mut stmt_insert = lock.prepare_cached(
|
||||
"INSERT INTO \"kv_store\"(\"kv_store_key\", \"kv_store_value\") VALUES(?1, ?2)",
|
||||
)?;
|
||||
if stmt_get.query_one(params![key], |_| Ok(())).is_ok() {
|
||||
stmt_set.execute(params![val, key])?;
|
||||
} else {
|
||||
stmt_insert.execute(params![key, val])?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn execute(
|
||||
conn: &mut rusqlite::Connection,
|
||||
sql: &str,
|
||||
params: &[&dyn rusqlite::ToSql],
|
||||
) -> Result<(), Error> {
|
||||
let mut stmt = conn.prepare_cached(sql).map_err(Error::make_internal)?;
|
||||
stmt.execute(params).map_err(Error::make_internal)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
mod api;
|
||||
mod auth;
|
||||
mod init_file;
|
||||
mod local;
|
||||
mod secret;
|
||||
mod util;
|
||||
|
||||
use crate::{local::LocalDb, secret::ResidentKeys};
|
||||
use clap::Parser;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
const DEFAULT_LIBEXEC_DIR: &str = match std::option_env!("DEFAULT_LIBEXEC_DIR") {
|
||||
Some(x) => x,
|
||||
None => "/usr/libexec/accountd",
|
||||
};
|
||||
const DEFAULT_DATA_DIR: &str = match std::option_env!("DEFAULT_DATA_DIR") {
|
||||
Some(x) => x,
|
||||
None => "/var/lib/accountd",
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct AppState {
|
||||
/// The `accountd` dedicated libexec directory, e.g. `/usr/libexec/accountd`.
|
||||
libexec_dir: PathBuf,
|
||||
|
||||
/// The `accountd` dedicated data directory, e.g. `/var/lib/accountd`.
|
||||
data_dir: PathBuf,
|
||||
|
||||
/// Local database.
|
||||
local_db: LocalDb,
|
||||
|
||||
/// Resident keys.
|
||||
resident_keys: ResidentKeys,
|
||||
}
|
||||
impl AppState {
|
||||
fn auth_methods(&self) -> Box<dyn Iterator<Item = auth::AuthExec>> {
|
||||
let Ok(tree_dir) = util::fs::TreeDir::open(&self.libexec_dir) else {
|
||||
return Box::new(std::iter::empty());
|
||||
};
|
||||
Box::new(
|
||||
tree_dir
|
||||
.filter(|x| x.file_name().to_string_lossy().starts_with("auth_"))
|
||||
.map(|x| auth::AuthExec::from_path_buf(x.path())),
|
||||
)
|
||||
}
|
||||
|
||||
fn auth_method(&self, name: &str) -> Option<auth::AuthExec> {
|
||||
self.auth_methods()
|
||||
.find(|x| matches!(x.name().as_deref(), Ok(x) if x == name))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
struct Cli {
|
||||
/// Specify libexec directory
|
||||
#[arg(long)]
|
||||
libexec_dir: Option<PathBuf>,
|
||||
|
||||
/// Specify data directory
|
||||
#[arg(long)]
|
||||
data_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() {
|
||||
let cli = Cli::parse();
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let libexec_dir = cli
|
||||
.libexec_dir
|
||||
.unwrap_or_else(|| DEFAULT_LIBEXEC_DIR.into());
|
||||
let data_dir = cli.data_dir.unwrap_or_else(|| DEFAULT_DATA_DIR.into());
|
||||
_ = std::fs::create_dir_all(&data_dir);
|
||||
|
||||
let local_db_path = data_dir.join("local.db");
|
||||
let local_db = match LocalDb::open(&local_db_path) {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
tracing::error!("failed to open local database: {e}",);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let app_state = Arc::new(AppState {
|
||||
libexec_dir,
|
||||
data_dir,
|
||||
local_db,
|
||||
resident_keys: ResidentKeys::new(),
|
||||
});
|
||||
|
||||
if app_state.local_db.is_first_run() {
|
||||
if init_file::run(app_state.clone()).await.is_err() {
|
||||
std::process::exit(1);
|
||||
}
|
||||
if let Err(e) = app_state.local_db.unset_first_run() {
|
||||
tracing::error!("failed to unset first run: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = api::launch_server(app_state).await {
|
||||
tracing::error!("failed to launch api server: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
while tokio::signal::ctrl_c().await.is_err() {}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
use crate::util::time::timestamp_s;
|
||||
use rustc_hash::FxHashMap;
|
||||
use semios_account::secret::MasterKeyProvision;
|
||||
use std::sync::Mutex;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ResidentKeys(Mutex<FxHashMap<Uuid, MasterKeyProvision>>);
|
||||
impl ResidentKeys {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn set(&self, uuid: Uuid, provision: MasterKeyProvision) {
|
||||
self.0.lock().unwrap().insert(uuid, provision);
|
||||
}
|
||||
|
||||
pub fn get(&self, uuid: &Uuid) -> Option<MasterKeyProvision> {
|
||||
let mut lock = self.0.lock().unwrap();
|
||||
let provision = lock.get(uuid)?;
|
||||
if provision.expiration_time < timestamp_s() {
|
||||
lock.remove(uuid);
|
||||
return None;
|
||||
}
|
||||
Some(provision.clone())
|
||||
}
|
||||
|
||||
pub fn remove(&self, uuid: &Uuid) {
|
||||
self.0.lock().unwrap().remove(uuid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use std::{
|
||||
fs::{DirEntry, ReadDir},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TreeDir(Vec<ReadDir>);
|
||||
impl TreeDir {
|
||||
pub fn open<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
|
||||
Ok(Self(vec![std::fs::read_dir(path)?]))
|
||||
}
|
||||
}
|
||||
impl Iterator for TreeDir {
|
||||
type Item = DirEntry;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
while let Some(current_dir) = self.0.last_mut() {
|
||||
match current_dir.next() {
|
||||
Some(Ok(entry)) => {
|
||||
if let Ok(file_type) = entry.file_type() {
|
||||
if file_type.is_dir() {
|
||||
if let Ok(sub_dir) = std::fs::read_dir(entry.path()) {
|
||||
self.0.push(sub_dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Some(entry);
|
||||
}
|
||||
Some(Err(_)) => {
|
||||
continue;
|
||||
}
|
||||
None => {
|
||||
self.0.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
//! Local IPC implementation -- Unix-like Operating Systems.
|
||||
|
||||
use anyhow::anyhow;
|
||||
use cfg_if::cfg_if;
|
||||
use semios_account::{
|
||||
protocol::{MAX_MESSAGE_LEN, Uri},
|
||||
record::HostUid,
|
||||
};
|
||||
use std::os::unix::net::{SocketAddr, UnixListener as StdUnixListener};
|
||||
use tokio::{
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
net::{UnixListener, UnixStream},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Listener(UnixListener);
|
||||
impl Listener {
|
||||
pub async fn new(uri: &str) -> anyhow::Result<Self> {
|
||||
let uri: Uri = uri.parse()?;
|
||||
let sockaddr = match uri {
|
||||
Uri::Unix(path) => {
|
||||
if path.exists() && tokio::net::UnixStream::connect(&path).await.is_err() {
|
||||
std::fs::remove_file(&path)?;
|
||||
}
|
||||
SocketAddr::from_pathname(path)?
|
||||
}
|
||||
Uri::UnixAbstract(_name) => {
|
||||
cfg_if! {
|
||||
if #[cfg(target_os = "linux")] {
|
||||
use std::os::linux::net::SocketAddrExt;
|
||||
SocketAddr::from_abstract_name(_name.as_bytes())?
|
||||
} else {
|
||||
return Err(anyhow!("abstract socket names are only supported on Linux"));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => return Err(anyhow!("only \"unix://\" ipc is supported on unix")),
|
||||
};
|
||||
let listener = StdUnixListener::bind_addr(&sockaddr)?;
|
||||
listener.set_nonblocking(true)?;
|
||||
Ok(Self(UnixListener::from_std(listener)?))
|
||||
}
|
||||
|
||||
pub async fn accept(&self) -> anyhow::Result<Connection> {
|
||||
Ok(Connection(self.0.accept().await?.0))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Connection(UnixStream);
|
||||
impl Connection {
|
||||
pub async fn recv(&mut self, data: &mut Vec<u8>) -> anyhow::Result<()> {
|
||||
let len = self.0.read_u32_le().await? as usize;
|
||||
if len > MAX_MESSAGE_LEN {
|
||||
return Err(anyhow!("message too large: {len} bytes"));
|
||||
}
|
||||
data.resize(len, 0);
|
||||
self.0.read_exact(data).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send(&mut self, data: &[u8]) -> anyhow::Result<()> {
|
||||
if data.len() > MAX_MESSAGE_LEN {
|
||||
return Err(anyhow!("message too large: {} bytes", data.len()));
|
||||
}
|
||||
self.0.write_u32_le(data.len() as u32).await?;
|
||||
self.0.write_all(data).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn peer_host_uid(&self) -> Option<HostUid> {
|
||||
Some(self.0.peer_cred().ok()?.uid())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
pub mod fs;
|
||||
pub mod time;
|
||||
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(target_family = "unix")] {
|
||||
#[path = "ipc_unix.rs"]
|
||||
pub mod ipc;
|
||||
} else {
|
||||
std::compile_error!("target not supported");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
use std::time::SystemTime;
|
||||
|
||||
macro_rules! timestamp {
|
||||
($f:ident) => {{
|
||||
let now = SystemTime::now();
|
||||
match now.duration_since(SystemTime::UNIX_EPOCH) {
|
||||
Ok(x) => x.$f() as i64,
|
||||
Err(_) => -(SystemTime::UNIX_EPOCH.duration_since(now).unwrap().$f() as i64),
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
pub fn timestamp_s() -> i64 {
|
||||
timestamp!(as_secs)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "auth_method_fx"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
semios_account = { path = "../semios_account" }
|
||||
serde_json = "1"
|
||||
@@ -0,0 +1,81 @@
|
||||
//! A framework for writing an auth method.
|
||||
|
||||
use semios_account::auth::{AuthCli, AuthMethodInfo, AuthenticateArgs, ModeInfo, UpdateArgs};
|
||||
|
||||
pub struct App {
|
||||
name: &'static str,
|
||||
provides_master_key: bool,
|
||||
modes: Vec<Box<dyn Mode>>,
|
||||
}
|
||||
impl App {
|
||||
#[inline]
|
||||
pub const fn new(name: &'static str) -> Self {
|
||||
Self {
|
||||
name,
|
||||
provides_master_key: false,
|
||||
modes: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn provides_master_key(mut self) -> Self {
|
||||
self.provides_master_key = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn mode(mut self, mode: Box<dyn Mode>) -> Self {
|
||||
self.modes.push(mode);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn run(self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
run(self)
|
||||
}
|
||||
|
||||
fn info(&self) -> AuthMethodInfo {
|
||||
AuthMethodInfo {
|
||||
name: self.name.into(),
|
||||
provides_master_key: self.provides_master_key,
|
||||
modes: self
|
||||
.modes
|
||||
.iter()
|
||||
.map(|x| ModeInfo {
|
||||
name: x.name().into(),
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Mode {
|
||||
fn name(&self) -> &'static str;
|
||||
fn authenticate(&self, args: AuthenticateArgs) -> Result<(), Box<dyn std::error::Error>>;
|
||||
fn update(&self, args: UpdateArgs) -> Result<(), Box<dyn std::error::Error>>;
|
||||
}
|
||||
|
||||
pub fn run(app: App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cli = AuthCli::parse(&std::env::args().skip(1).collect::<Vec<_>>())?;
|
||||
match cli {
|
||||
AuthCli::QueryInformation => query_info(&app),
|
||||
AuthCli::Authenticate(args) => authenticate(&app, args),
|
||||
AuthCli::Update(args) => update(&app, args),
|
||||
}
|
||||
}
|
||||
|
||||
fn query_info(app: &App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("{}", serde_json::to_string(&app.info())?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn authenticate(app: &App, args: AuthenticateArgs) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let Some(mode) = app.modes.iter().find(|x| x.name() == &args.mode) else {
|
||||
return Err(Box::from("mode not found"));
|
||||
};
|
||||
mode.authenticate(args)
|
||||
}
|
||||
|
||||
fn update(app: &App, args: UpdateArgs) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let Some(mode) = app.modes.iter().find(|x| x.name() == &args.mode) else {
|
||||
return Err(Box::from("mode not found"));
|
||||
};
|
||||
mode.update(args)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "semios_account"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
default = ["client"]
|
||||
client = []
|
||||
|
||||
[dependencies]
|
||||
cfg-if = "1"
|
||||
bitflags = { version = "2", features = ["serde"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
thiserror = "2"
|
||||
uuid = { version = "1", features = ["serde"] }
|
||||
@@ -0,0 +1,161 @@
|
||||
//! User authentication.
|
||||
|
||||
use bitflags::bitflags;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
macro_rules! decl_session_args {
|
||||
(pub struct $sn:ident { $(pub $n:ident : $t:ty,)* }) => {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct $sn {
|
||||
$(pub $n: $t),*
|
||||
}
|
||||
impl $sn {
|
||||
pub fn parse(args: &[String]) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
Self::_from_session_args(SessionArgs::parse(args)?)
|
||||
}
|
||||
|
||||
pub fn compose(&self) -> Vec<String> {
|
||||
self._to_session_args().compose()
|
||||
}
|
||||
|
||||
fn _from_session_args(sa: SessionArgs) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
Ok(Self {
|
||||
$(
|
||||
$n: sa.0.get(stringify!($n))
|
||||
.ok_or_else(|| Box::<dyn std::error::Error>::from(format!(
|
||||
"argument {} not found", stringify!($n),
|
||||
)))?
|
||||
.parse::<$t>()?
|
||||
),*
|
||||
})
|
||||
}
|
||||
|
||||
fn _to_session_args(&self) -> SessionArgs {
|
||||
let mut map = HashMap::new();
|
||||
$(
|
||||
map.insert(stringify!($n).into(), self.$n.to_string());
|
||||
)*
|
||||
SessionArgs(map)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Maximum count of auth methods enabled for one user.
|
||||
pub const USER_MAX_AUTH_METHODS: usize = 32;
|
||||
|
||||
/// Record of an auth method enabled for a user.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UserAuthMethodRecord {
|
||||
/// Name of the auth method.
|
||||
pub name: String,
|
||||
|
||||
/// Flags of the auth method.
|
||||
pub flags: UserAuthMethodFlags,
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct UserAuthMethodFlags: u32 {
|
||||
/// Indicates if the auth method is a "main" auth method.
|
||||
///
|
||||
/// A "main" auth method is unique for a user. It provides a master key for secret storage.
|
||||
const MAIN = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about an authentication method.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AuthMethodInfo {
|
||||
/// Name of the auth method.
|
||||
pub name: String,
|
||||
|
||||
/// True if the auth method supports providing the master key.
|
||||
pub provides_master_key: bool,
|
||||
|
||||
/// Supported operation modes.
|
||||
pub modes: Vec<ModeInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ModeInfo {
|
||||
/// Name of the operation mode.
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// Command line of an authentication method provider.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AuthCli {
|
||||
/// Queries information about the auth method.
|
||||
///
|
||||
/// The implementor should output an [`AuthMethod`] in JSON to stdout stream, and then exit.
|
||||
QueryInformation,
|
||||
|
||||
/// Begins an authentication session.
|
||||
Authenticate(AuthenticateArgs),
|
||||
|
||||
/// Begins an update session.
|
||||
Update(UpdateArgs),
|
||||
}
|
||||
impl AuthCli {
|
||||
pub fn parse(args: &[String]) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
match args.first().map(String::as_str) {
|
||||
Some("QueryInformation") => Ok(Self::QueryInformation),
|
||||
Some("Authenticate") => Ok(Self::Authenticate(AuthenticateArgs::parse(&args[1..])?)),
|
||||
Some("Update") => Ok(Self::Update(UpdateArgs::parse(&args[1..])?)),
|
||||
Some(unexpected) => Err(Box::from(format!("unexpected command `{unexpected}`"))),
|
||||
None => Err(Box::from("no command specified")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compose(&self) -> Vec<String> {
|
||||
match self {
|
||||
Self::QueryInformation => vec!["QueryInformation".into()],
|
||||
Self::Authenticate(args) => {
|
||||
let mut ret = vec!["Authenticate".into()];
|
||||
ret.append(&mut args.compose());
|
||||
ret
|
||||
}
|
||||
Self::Update(args) => {
|
||||
let mut ret = vec!["Update".into()];
|
||||
ret.append(&mut args.compose());
|
||||
ret
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
decl_session_args! {
|
||||
pub struct AuthenticateArgs {
|
||||
pub user_uuid: Uuid,
|
||||
pub mode: String,
|
||||
}
|
||||
}
|
||||
|
||||
decl_session_args! {
|
||||
pub struct UpdateArgs {
|
||||
pub user_uuid: Uuid,
|
||||
pub mode: String,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct SessionArgs(HashMap<String, String>);
|
||||
impl SessionArgs {
|
||||
fn parse(args: &[String]) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let mut map = HashMap::with_capacity(args.len());
|
||||
for arg in args {
|
||||
let Some((key, val)) = arg.split_once('=') else {
|
||||
return Err(Box::from("invalid argument format"));
|
||||
};
|
||||
map.insert(key.into(), val.into());
|
||||
}
|
||||
Ok(Self(map))
|
||||
}
|
||||
|
||||
fn compose(&self) -> Vec<String> {
|
||||
self.0.iter().map(|(k, v)| format!("{k}={v}")).collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
use crate::{
|
||||
auth::UserAuthMethodRecord,
|
||||
error::Error,
|
||||
protocol::*,
|
||||
raw_client::Connection,
|
||||
record::{GroupInfo, UserInfo},
|
||||
secret::Secret,
|
||||
};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Client {
|
||||
conn: Connection,
|
||||
buf: Vec<u8>,
|
||||
}
|
||||
impl Client {
|
||||
pub fn connect_default() -> std::io::Result<Self> {
|
||||
Self::connect(&crate::protocol::uri())
|
||||
}
|
||||
|
||||
pub fn connect(uri: &str) -> std::io::Result<Self> {
|
||||
Ok(Self {
|
||||
conn: Connection::connect(uri)?,
|
||||
buf: Vec::with_capacity(512),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn invoke<P: Serialize, R: DeserializeOwned>(
|
||||
&mut self,
|
||||
method: String,
|
||||
params: P,
|
||||
) -> Result<R, Error> {
|
||||
self.buf.clear();
|
||||
let params =
|
||||
serde_json::to_value(params).map_err(|e| Error::Communication(e.to_string()))?;
|
||||
let req = Request { method, params };
|
||||
serde_json::to_writer(&mut self.buf, &req)
|
||||
.map_err(|e| Error::Communication(e.to_string()))?;
|
||||
self.conn
|
||||
.send(&self.buf)
|
||||
.map_err(|e| Error::Communication(e.to_string()))?;
|
||||
self.conn
|
||||
.recv(&mut self.buf)
|
||||
.map_err(|e| Error::Communication(e.to_string()))?;
|
||||
let resp: Response =
|
||||
serde_json::from_slice(&self.buf).map_err(|e| Error::Communication(e.to_string()))?;
|
||||
resp.into_result()
|
||||
.map(|x| serde_json::from_value(x).map_err(|e| Error::Communication(e.to_string())))
|
||||
.flatten()
|
||||
}
|
||||
|
||||
pub fn get_user_info(&mut self, args: GetUserInfoArgs) -> Result<UserInfo, Error> {
|
||||
self.invoke(GET_USER_INFO.into(), args)
|
||||
}
|
||||
|
||||
pub fn get_group_info(&mut self, args: GetGroupInfoArgs) -> Result<GroupInfo, Error> {
|
||||
self.invoke(GET_GROUP_INFO.into(), args)
|
||||
}
|
||||
|
||||
pub fn get_secret(&mut self, args: GetSecretArgs) -> Result<Secret, Error> {
|
||||
self.invoke(GET_SECRET.into(), args)
|
||||
}
|
||||
|
||||
pub fn set_secret(&mut self, args: SetSecretArgs) -> Result<(), Error> {
|
||||
self.invoke(SET_SECRET.into(), args)
|
||||
}
|
||||
|
||||
pub fn provide_master_key(&mut self, args: ProvideMasterKeyArgs) -> Result<(), Error> {
|
||||
self.invoke(PROVIDE_MASTER_KEY.into(), args)
|
||||
}
|
||||
|
||||
pub fn clear_master_key(&mut self, args: ClearMasterKeyArgs) -> Result<(), Error> {
|
||||
self.invoke(CLEAR_MASTER_KEY.into(), args)
|
||||
}
|
||||
|
||||
pub fn get_user_auth_methods(
|
||||
&mut self,
|
||||
args: GetUserAuthMethodsArgs,
|
||||
) -> Result<Vec<UserAuthMethodRecord>, Error> {
|
||||
self.invoke(GET_USER_AUTH_METHODS.into(), args)
|
||||
}
|
||||
|
||||
pub fn get_auth_method_path(&mut self, args: GetAuthMethodPathArgs) -> Result<PathBuf, Error> {
|
||||
self.invoke(GET_AUTH_METHOD_PATH.into(), args)
|
||||
}
|
||||
|
||||
pub fn user_add_auth_method(&mut self, args: UserAddAuthMethodArgs) -> Result<(), Error> {
|
||||
self.invoke(USER_ADD_AUTH_METHOD.into(), args)
|
||||
}
|
||||
|
||||
pub fn create_user(&mut self, args: CreateUserArgs) -> Result<(), Error> {
|
||||
self.invoke(CREATE_USER.into(), args)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
//! Errors.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Display;
|
||||
|
||||
/// An error.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// Communication error.
|
||||
#[error("communication error: {0}")]
|
||||
Communication(String),
|
||||
|
||||
/// The specified method is not implemented.
|
||||
#[error("`{0}`: Not implemented")]
|
||||
NotImplemented(String),
|
||||
|
||||
/// Parameters are not suitable for the called method.
|
||||
#[error("invalid parameters: {0}")]
|
||||
InvalidParams(String),
|
||||
|
||||
/// The user has not enough permission to perform the action.
|
||||
#[error("permission denied")]
|
||||
PermissionDenied,
|
||||
|
||||
/// The operation operates on a user, but the specified user does not exist.
|
||||
#[error("no such user")]
|
||||
NoSuchUser,
|
||||
|
||||
/// The operation operates on a group, but the specified group does not exist.
|
||||
#[error("no such group")]
|
||||
NoSuchGroup,
|
||||
|
||||
/// The operation operates on a secret, but the specified secret does not exist.
|
||||
#[error("no such secret")]
|
||||
NoSuchSecret,
|
||||
|
||||
/// Attempted to launch an auth method that is not installed on current system.
|
||||
#[error("no such auth method")]
|
||||
NoSuchAuthMethod,
|
||||
|
||||
/// Attempted to authenticate/update authentication via an auth method that is not activated for the user.
|
||||
///
|
||||
/// **NOTE**: This does not imply that the auth method is installed on current system.
|
||||
#[error("the specified auth method is not activated for the user")]
|
||||
NotActivatedAuthMethod,
|
||||
|
||||
/// TODO
|
||||
#[error("already exists")]
|
||||
AlreadyExists,
|
||||
|
||||
/// An internal error.
|
||||
#[error("internal error: {0}")]
|
||||
Internal(String),
|
||||
}
|
||||
impl Error {
|
||||
pub fn make_internal(x: impl Display) -> Self {
|
||||
Self::Internal(x.to_string())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
pub mod auth;
|
||||
pub mod error;
|
||||
pub mod protocol;
|
||||
pub mod record;
|
||||
pub mod secret;
|
||||
pub mod wellknown;
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(target_family = "unix")] {
|
||||
#[path = "raw_client_unix.rs"]
|
||||
pub mod raw_client;
|
||||
} else {
|
||||
std::compile_error!("target not supported");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
pub mod client;
|
||||
@@ -0,0 +1,266 @@
|
||||
use crate::{
|
||||
error::Error,
|
||||
record::{GroupInfo, HostGid, HostUid, SecureTag},
|
||||
secret::{MasterKeyProvision, Secret},
|
||||
};
|
||||
use cfg_if::cfg_if;
|
||||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Max length of a single message.
|
||||
pub const MAX_MESSAGE_LEN: usize = 128 * 1024;
|
||||
|
||||
// ==- IPC URI -==
|
||||
cfg_if! {
|
||||
if #[cfg(target_os = "linux")] {
|
||||
pub const DEFAULT_URI: &str = "unix://@verified:org.semilabs.os/accountd";
|
||||
} else if #[cfg(target_family = "unix")] {
|
||||
pub const DEFAULT_URI: &str = "unix:///var/run/accountd.sock";
|
||||
} else {
|
||||
pub const DEFAULT_URI: &str = "";
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub enum Uri {
|
||||
Unix(std::path::PathBuf),
|
||||
UnixAbstract(String),
|
||||
}
|
||||
impl std::str::FromStr for Uri {
|
||||
type Err = std::io::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if let Some(unix) = s.strip_prefix("unix://") {
|
||||
if unix.starts_with('/') {
|
||||
Ok(Self::Unix(unix.into()))
|
||||
} else if unix.starts_with('@') {
|
||||
Ok(Self::UnixAbstract(unix[1..].into()))
|
||||
} else {
|
||||
Err(std::io::ErrorKind::AddrNotAvailable.into())
|
||||
}
|
||||
} else {
|
||||
Err(std::io::ErrorKind::AddrNotAvailable.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets IPC URI.
|
||||
pub fn uri() -> String {
|
||||
std::env::var("ACCOUNT_IPC_URI").unwrap_or_else(|_| DEFAULT_URI.into())
|
||||
}
|
||||
|
||||
// ==- Basic Definitions -==
|
||||
|
||||
/// A request.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Request {
|
||||
pub method: String,
|
||||
pub params: serde_json::Value,
|
||||
}
|
||||
impl Request {
|
||||
pub fn params<T: DeserializeOwned>(self) -> Result<T, Error> {
|
||||
serde_json::from_value(self.params).map_err(|e| Error::InvalidParams(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// A response.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Response {
|
||||
pub success: bool,
|
||||
pub value: serde_json::Value,
|
||||
}
|
||||
impl Response {
|
||||
pub fn from_result<R: Serialize>(result: Result<R, Error>) -> Self {
|
||||
let success = result.is_ok();
|
||||
let value = match result {
|
||||
Ok(x) => serde_json::to_value(x).expect("response not serialized"),
|
||||
Err(e) => serde_json::to_value(e).expect("response not serialized"),
|
||||
};
|
||||
Self { success, value }
|
||||
}
|
||||
|
||||
pub fn into_result<R: DeserializeOwned>(self) -> Result<R, Error> {
|
||||
if self.success {
|
||||
Ok(serde_json::from_value(self.value)
|
||||
.map_err(|x| Error::InvalidParams(x.to_string()))?)
|
||||
} else {
|
||||
Err(serde_json::from_value(self.value)
|
||||
.map_err(|x| Error::InvalidParams(x.to_string()))?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==- Method Names: Querying Server Information -==
|
||||
|
||||
pub const SERVER_VERSION: &str = "ServerVersion";
|
||||
|
||||
// ==- Methods: Querying Records -==
|
||||
|
||||
pub const LIST_USER: &str = "ListUser";
|
||||
pub const LIST_GROUP: &str = "ListGroup";
|
||||
pub const GET_USER_INFO: &str = "GetUserInfo";
|
||||
pub const GET_GROUP_INFO: &str = "GetGroupInfo";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ListUserArgs {
|
||||
pub start: u32,
|
||||
pub len: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ListGroupArgs {
|
||||
pub start: u32,
|
||||
pub len: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GetUserInfoArgs {
|
||||
Uuid(Uuid),
|
||||
Username(String),
|
||||
HostUid(HostUid),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GetGroupInfoArgs {
|
||||
Uuid(Uuid),
|
||||
Groupname(String),
|
||||
HostGid(HostGid),
|
||||
}
|
||||
|
||||
// ==- Methods: Managed Secrets -==
|
||||
|
||||
pub const GET_SECRET: &str = "GetSecret";
|
||||
pub const SET_SECRET: &str = "SetSecret";
|
||||
pub const REMOVE_SECRET: &str = "RemoveSecret";
|
||||
pub const PROVIDE_MASTER_KEY: &str = "ProvideMasterKey";
|
||||
pub const CLEAR_MASTER_KEY: &str = "ClearMasterKey";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GetSecretArgs {
|
||||
pub user: Uuid,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SetSecretArgs {
|
||||
pub user: Uuid,
|
||||
pub secret: Secret,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RemoveSecretArgs {
|
||||
pub user: Uuid,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProvideMasterKeyArgs {
|
||||
pub user: Uuid,
|
||||
pub provision: MasterKeyProvision,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ClearMasterKeyArgs {
|
||||
pub user: Uuid,
|
||||
}
|
||||
|
||||
// ==- Methods: User Management -==
|
||||
|
||||
pub const CREATE_USER: &str = "CreateUser";
|
||||
pub const REMOVE_USER: &str = "RemoveUser";
|
||||
pub const CREATE_GROUP: &str = "CreateGroup";
|
||||
pub const REMOVE_GROUP: &str = "RemoveGroup";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateUserArgs {
|
||||
/// User name.
|
||||
pub username: String,
|
||||
|
||||
/// Full name.
|
||||
pub fullname: Option<String>,
|
||||
|
||||
/// Specify a host UID.
|
||||
pub host_uid: Option<HostUid>,
|
||||
|
||||
/// User description.
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
|
||||
/// Secure tags.
|
||||
#[serde(default)]
|
||||
pub secure_tags: HashSet<SecureTag>,
|
||||
|
||||
/// Extra records.
|
||||
#[serde(default)]
|
||||
pub extra_records: HashMap<String, String>,
|
||||
|
||||
/// Defaults of the user.
|
||||
#[serde(default)]
|
||||
pub defaults: HashMap<String, String>,
|
||||
|
||||
/// Home directory of the user.
|
||||
pub home_directory: Option<String>,
|
||||
|
||||
/// Initial groups of the newly created user.
|
||||
#[serde(default)]
|
||||
pub groups: Vec<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RemoveUserArgs {
|
||||
/// UUID of the user to be removed.
|
||||
pub uuid: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateGroupArgs {
|
||||
/// Group name.
|
||||
pub groupname: String,
|
||||
|
||||
/// Full group name.
|
||||
pub fullname: Option<String>,
|
||||
|
||||
/// Group ID on current host.
|
||||
pub host_gid: Option<HostGid>,
|
||||
|
||||
/// Group description.
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RemoveGroupArgs {
|
||||
/// UUID of the group to be removed.
|
||||
pub uuid: Uuid,
|
||||
}
|
||||
|
||||
// ==- Methods: Authentication -==
|
||||
|
||||
pub const GET_USER_AUTH_METHODS: &str = "GetUserAuthMethods";
|
||||
pub const GET_AUTH_METHOD_PATH: &str = "GetAuthMethodPath";
|
||||
pub const USER_ADD_AUTH_METHOD: &str = "UserAddAuthMethod";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GetUserAuthMethodsArgs {
|
||||
/// UUID of the user.
|
||||
pub user: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GetAuthMethodPathArgs {
|
||||
/// Name of the auth method.
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UserAddAuthMethodArgs {
|
||||
/// UUID of the user.
|
||||
pub user: Uuid,
|
||||
|
||||
/// Name of the auth method.
|
||||
pub auth_method: String,
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
use crate::protocol::{MAX_MESSAGE_LEN, Uri};
|
||||
use cfg_if::cfg_if;
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
os::unix::net::{SocketAddr, UnixStream},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Connection(UnixStream);
|
||||
impl Connection {
|
||||
pub fn connect(uri: &str) -> std::io::Result<Self> {
|
||||
let uri: Uri = uri.parse()?;
|
||||
let sockaddr = match uri {
|
||||
Uri::Unix(path) => SocketAddr::from_pathname(path)?,
|
||||
Uri::UnixAbstract(_name) => {
|
||||
cfg_if! {
|
||||
if #[cfg(target_os = "linux")] {
|
||||
use std::os::linux::net::SocketAddrExt;
|
||||
SocketAddr::from_abstract_name(_name.as_bytes())?
|
||||
} else {
|
||||
return Err(std::io::ErrorKind::Unsupported.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(Self(UnixStream::connect_addr(&sockaddr)?))
|
||||
}
|
||||
|
||||
pub fn send(&mut self, data: &[u8]) -> std::io::Result<()> {
|
||||
if data.len() > MAX_MESSAGE_LEN {
|
||||
return Err(std::io::ErrorKind::FileTooLarge.into());
|
||||
}
|
||||
self.0.write_all(&(data.len() as u32).to_le_bytes())?;
|
||||
self.0.write(data)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn recv(&mut self, buf: &mut Vec<u8>) -> std::io::Result<()> {
|
||||
let mut len = [0u8; size_of::<u32>()];
|
||||
self.0.read_exact(&mut len)?;
|
||||
let len = u32::from_le_bytes(len) as usize;
|
||||
if len > MAX_MESSAGE_LEN {
|
||||
return Err(std::io::ErrorKind::FileTooLarge.into());
|
||||
}
|
||||
buf.resize(len, 0);
|
||||
self.0.read_exact(buf)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
//! Basic user information records.
|
||||
|
||||
use cfg_if::cfg_if;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use uuid::Uuid;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(target_family = "unix")] {
|
||||
pub type HostUid = u32;
|
||||
pub type HostGid = u32;
|
||||
} else if #[cfg(target_family = "windows")] {
|
||||
pub type HostUid = String;
|
||||
pub type HostGid = String;
|
||||
} else {
|
||||
pub type HostUid = u64;
|
||||
pub type HostGid = u64;
|
||||
}
|
||||
}
|
||||
|
||||
/// Basical user information.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UserInfo {
|
||||
/// User UUID.
|
||||
pub uuid: Uuid,
|
||||
|
||||
/// Username.
|
||||
pub username: String,
|
||||
|
||||
/// User ID on current host.
|
||||
pub host_uid: Option<HostUid>,
|
||||
|
||||
/// Full name.
|
||||
pub fullname: String,
|
||||
|
||||
/// User description.
|
||||
pub description: String,
|
||||
|
||||
/// Secure tags.
|
||||
pub secure_tags: HashSet<SecureTag>,
|
||||
|
||||
/// Extra records.
|
||||
pub extra_records: HashMap<String, String>,
|
||||
|
||||
/// Defaults of the user.
|
||||
pub defaults: HashMap<String, String>,
|
||||
|
||||
/// Home directory of the user.
|
||||
pub home_directory: Option<String>,
|
||||
|
||||
/// Time the user is created.
|
||||
pub creation_time: i64,
|
||||
|
||||
/// Time the user is last logged in.
|
||||
pub last_login_time: i64,
|
||||
}
|
||||
impl UserInfo {
|
||||
pub fn clear_host_specific(&mut self) {
|
||||
self.host_uid = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// User secure tag.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Hash, PartialEq, Eq)]
|
||||
pub enum SecureTag {
|
||||
/// Indicates that the user can be logged in.
|
||||
Login,
|
||||
|
||||
/// Indicates that the user may create another user.
|
||||
CreateUser,
|
||||
|
||||
/// Indicates that the user may remove users.
|
||||
RemoveUser,
|
||||
|
||||
/// Read secrets of other users.
|
||||
ReadSecret,
|
||||
|
||||
/// Set or delete secrets of other users.
|
||||
WriteSecret,
|
||||
}
|
||||
|
||||
/// Basical group information.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GroupInfo {
|
||||
/// Group UUID.
|
||||
pub uuid: Uuid,
|
||||
|
||||
/// Group name.
|
||||
pub groupname: String,
|
||||
|
||||
/// Group ID on current host.
|
||||
pub host_gid: Option<HostGid>,
|
||||
|
||||
/// Full group name.
|
||||
pub fullname: String,
|
||||
|
||||
/// Group description.
|
||||
pub description: String,
|
||||
|
||||
/// Time the group is created.
|
||||
pub creation_time: i64,
|
||||
}
|
||||
impl GroupInfo {
|
||||
pub fn clear_host_specific(&mut self) {
|
||||
self.host_gid = None;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
//! User managed secrets.
|
||||
//!
|
||||
//! Managed Secrets is a feature that allows applications host their secrets (e.g. password database keys) here,
|
||||
//! and get the secrets later with proper authentication, like Apple Keychain and KDE KWallet.
|
||||
|
||||
use bitflags::bitflags;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Maximum length of a secret name.
|
||||
pub const SECRET_NAME_LEN: usize = 1024;
|
||||
|
||||
/// Maximum length of a managed secret item.
|
||||
pub const SECRET_MAX_LEN: usize = 4096;
|
||||
|
||||
/// Maximum count of managed secrets owned by a user.
|
||||
pub const MAX_SECRET_COUNT: usize = 16384;
|
||||
|
||||
/// A managed secret item.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Secret {
|
||||
/// Name of the secret.
|
||||
pub name: String,
|
||||
|
||||
/// Data of the secret.
|
||||
pub data: Vec<u8>,
|
||||
|
||||
/// Security flags of the secret.
|
||||
pub security_flags: SecurityFlags,
|
||||
|
||||
/// Time the secret is created.
|
||||
pub creation_time: i64,
|
||||
|
||||
/// Time the secret will be expired.
|
||||
pub expiration_time: i64,
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct SecurityFlags: u32 {
|
||||
const ENCRYPTED = 1;
|
||||
const WRITE_PROTECTED = 2;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MasterKeyProvision {
|
||||
pub algorithm: String,
|
||||
pub key: Vec<u8>,
|
||||
pub expiration_time: i64,
|
||||
}
|
||||
impl MasterKeyProvision {
|
||||
pub const ALGORITHM_AES_256_GCM: &str = "AES-256-GCM";
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//! "Well-known" strings.
|
||||
|
||||
/// Command-line login shell. Used in "defaults" field of a user.
|
||||
pub const CLI_LOGIN_SHELL: &str = "CliLoginShell";
|
||||
|
||||
/// Birth date. Used in "extra_records" field of a user.
|
||||
pub const BIRTH_DATE: &str = "BirthDate";
|
||||
|
||||
/// Gender. Used in "extra_records" field of a user.
|
||||
pub const GENDER: &str = "Gender";
|
||||
|
||||
/// Name of the GUI interactive operation mode.
|
||||
pub const OP_MODE_GUI: &str = "GUI";
|
||||
|
||||
/// Name of the CLI interactive operation mode.
|
||||
pub const OP_MODE_CLI: &str = "CLI";
|
||||
|
||||
/// Name of the API non-interactive operation mode.
|
||||
pub const OP_MODE_API: &str = "API";
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "auth_password"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
semios_account = { path = "../../lib/semios_account", features = ["client"] }
|
||||
auth_method_fx = { path = "../../lib/auth_method_fx" }
|
||||
password-hash = { version = "0.6", features = ["phc", "rand_core", "getrandom"] }
|
||||
argon2 = "0.6.0-rc.8"
|
||||
uuid = "1"
|
||||
dialoguer = "0.12.0"
|
||||
rand = "0.10"
|
||||
aes-gcm = "0.10.3"
|
||||
generic-array = "0.14.7"
|
||||
@@ -0,0 +1,25 @@
|
||||
use auth_method_fx::Mode;
|
||||
use semios_account::{
|
||||
auth::{AuthenticateArgs, UpdateArgs},
|
||||
wellknown::OP_MODE_API,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ApiMode;
|
||||
impl Mode for ApiMode {
|
||||
fn name(&self) -> &'static str {
|
||||
OP_MODE_API
|
||||
}
|
||||
|
||||
fn authenticate(
|
||||
&self,
|
||||
AuthenticateArgs { user_uuid, mode }: AuthenticateArgs,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
debug_assert_eq!(mode, self.name());
|
||||
todo!();
|
||||
}
|
||||
|
||||
fn update(&self, args: UpdateArgs) -> Result<(), Box<dyn std::error::Error>> {
|
||||
todo!();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
//! The backend implementation.
|
||||
|
||||
use aes_gcm::{
|
||||
Aes256Gcm as AesGcm, Nonce,
|
||||
aead::{Aead, KeyInit},
|
||||
};
|
||||
use argon2::{ARGON2D_IDENT, ARGON2I_IDENT, ARGON2ID_IDENT, PasswordHash, PasswordVerifier};
|
||||
use generic_array::GenericArray;
|
||||
use password_hash::PasswordHasher;
|
||||
use rand::Rng;
|
||||
use semios_account::{
|
||||
client::Client,
|
||||
protocol::{GetSecretArgs, SetSecretArgs},
|
||||
secret::{MasterKeyProvision, Secret, SecurityFlags},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Default algorithm to hash new password strings.
|
||||
type PasswordHashAlgorithm = argon2::Argon2<'static>;
|
||||
|
||||
/// Name of secret used to store the hashed password string.
|
||||
const SECRET_NAME_PASSWORD: &str = "org.semilabs.os.account.auth.password/Password";
|
||||
|
||||
/// Name of secret used to store the encrypted (in this crate) master key.
|
||||
const SECRET_NAME_MASTER_KEY: &str = "org.semilabs.os.account.auth.password/MasterKey";
|
||||
|
||||
/// List of supported crypto algorithms.
|
||||
const CRYPTO_ALGO_LIST: &[&dyn CryptoAlgo] = &[&Aes256Gcm];
|
||||
|
||||
/// Default crypto algorithm.
|
||||
const DEFAULT_CRYPTO_ALGO: &dyn CryptoAlgo = CRYPTO_ALGO_LIST[0];
|
||||
|
||||
/// Updates password of a user.
|
||||
pub fn update(
|
||||
uuid: &Uuid,
|
||||
old_password: &str,
|
||||
new_password: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut client = Client::connect_default()?;
|
||||
|
||||
let crypto_algo;
|
||||
let clear_text_key;
|
||||
let master_key_secret = client.get_secret(GetSecretArgs {
|
||||
user: uuid.clone(),
|
||||
name: SECRET_NAME_MASTER_KEY.into(),
|
||||
});
|
||||
if let Ok(master_key_secret) = &master_key_secret {
|
||||
let (algo, encrypted_key) = extract_secret_master_key_data(&master_key_secret.data)?;
|
||||
crypto_algo = find_crypto(algo)?;
|
||||
clear_text_key = decrypt_key(crypto_algo, encrypted_key, old_password)?;
|
||||
} else {
|
||||
crypto_algo = DEFAULT_CRYPTO_ALGO;
|
||||
clear_text_key = DEFAULT_CRYPTO_ALGO.generate_key();
|
||||
}
|
||||
let reencrypted_key = encrypt_key(crypto_algo, &clear_text_key, new_password)?;
|
||||
|
||||
client.set_secret(SetSecretArgs {
|
||||
user: uuid.clone(),
|
||||
secret: compose_secret_master_key(crypto_algo, &reencrypted_key),
|
||||
})?;
|
||||
client.set_secret(SetSecretArgs {
|
||||
user: uuid.clone(),
|
||||
secret: compose_secret_password(new_password)?,
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verifies if the provided password can login the user represented by the provided UUID.
|
||||
pub fn verify(uuid: &Uuid, password: &str) -> Result<bool, Box<dyn std::error::Error>> {
|
||||
let mut client = Client::connect_default()?;
|
||||
let secret = client.get_secret(GetSecretArgs {
|
||||
user: uuid.clone(),
|
||||
name: SECRET_NAME_PASSWORD.into(),
|
||||
})?;
|
||||
verify_secret_password(&secret, password)
|
||||
}
|
||||
|
||||
/// Provides master key of a user.
|
||||
pub fn provide_master_key(
|
||||
uuid: &Uuid,
|
||||
password: &str,
|
||||
) -> Result<MasterKeyProvision, Box<dyn std::error::Error>> {
|
||||
let mut client = Client::connect_default()?;
|
||||
let secret = client.get_secret(GetSecretArgs {
|
||||
user: uuid.clone(),
|
||||
name: SECRET_NAME_MASTER_KEY.into(),
|
||||
})?;
|
||||
let (algo, encrypted_key) = extract_secret_master_key_data(&secret.data)?;
|
||||
let algo = find_crypto(algo)?;
|
||||
let key = decrypt_key(algo, encrypted_key, password)?;
|
||||
Ok(MasterKeyProvision {
|
||||
algorithm: algo.name().into(),
|
||||
key,
|
||||
expiration_time: i64::MAX,
|
||||
})
|
||||
}
|
||||
|
||||
/// Verifies if the password matches the managed secret item.
|
||||
///
|
||||
/// # Errors
|
||||
/// This function would return an error if:
|
||||
///
|
||||
/// - the PHC is invalid;
|
||||
/// - the algorithm used is unsupported;
|
||||
/// - the secret has invalid permission settings.
|
||||
fn verify_secret_password(
|
||||
secret: &Secret,
|
||||
password: &str,
|
||||
) -> Result<bool, Box<dyn std::error::Error>> {
|
||||
if !secret
|
||||
.security_flags
|
||||
.contains(SecurityFlags::WRITE_PROTECTED)
|
||||
{
|
||||
return Err(Box::from("insecure password secret"));
|
||||
}
|
||||
let phc = str::from_utf8(&secret.data)?;
|
||||
verify_phc(phc, password)
|
||||
}
|
||||
|
||||
/// Verifies if the password matches the PHC text.
|
||||
///
|
||||
/// # Errors
|
||||
/// This function would return an error if the PHC is invalid, or uses an unsupported algorithm.
|
||||
fn verify_phc(phc: &str, password: &str) -> Result<bool, Box<dyn std::error::Error>> {
|
||||
let phc = PasswordHash::new(phc)?;
|
||||
match phc.algorithm {
|
||||
ARGON2D_IDENT | ARGON2ID_IDENT | ARGON2I_IDENT => Ok(argon2::Argon2::default()
|
||||
.verify_password(password.as_bytes(), &phc)
|
||||
.is_ok()),
|
||||
_ => Err(Box::from("Unsupported algorithm")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Compose a [`Secret`] representing to the given password text.
|
||||
fn compose_secret_password(password: &str) -> Result<Secret, Box<dyn std::error::Error>> {
|
||||
Ok(Secret {
|
||||
name: SECRET_NAME_PASSWORD.into(),
|
||||
data: compose_phc(password)?.into_bytes(),
|
||||
security_flags: SecurityFlags::WRITE_PROTECTED,
|
||||
creation_time: 0,
|
||||
expiration_time: i64::MAX,
|
||||
})
|
||||
}
|
||||
|
||||
/// Composes a PHC string, hashing the provided password text using the default algorithm.
|
||||
fn compose_phc(password: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let algorithm = PasswordHashAlgorithm::default();
|
||||
Ok(algorithm.hash_password(password.as_bytes())?.to_string())
|
||||
}
|
||||
|
||||
/// Extracts algorithm and encrypted key from master key secret data.
|
||||
fn extract_secret_master_key_data(
|
||||
data: &[u8],
|
||||
) -> Result<(&str, &[u8]), Box<dyn std::error::Error>> {
|
||||
let zero_position = data.iter().position(|x| *x == 0).unwrap_or_default();
|
||||
if zero_position == 0 {
|
||||
return Err(Box::from("invalid master key format"));
|
||||
}
|
||||
let Ok(algo) = str::from_utf8(&data[..zero_position]) else {
|
||||
return Err(Box::from("invalid master key format"));
|
||||
};
|
||||
Ok((algo, &data[zero_position + 1..]))
|
||||
}
|
||||
|
||||
fn compose_secret_master_key(algorithm: &dyn CryptoAlgo, encrypted_key: &[u8]) -> Secret {
|
||||
Secret {
|
||||
name: SECRET_NAME_MASTER_KEY.into(),
|
||||
data: compose_secret_master_key_data(algorithm, encrypted_key),
|
||||
security_flags: SecurityFlags::WRITE_PROTECTED,
|
||||
creation_time: 0,
|
||||
expiration_time: i64::MAX,
|
||||
}
|
||||
}
|
||||
|
||||
/// Composes data of the [`Secret`] representing to the master key.
|
||||
fn compose_secret_master_key_data(algorithm: &dyn CryptoAlgo, encrypted_key: &[u8]) -> Vec<u8> {
|
||||
[algorithm.name().as_bytes(), encrypted_key].join(&b'\0')
|
||||
}
|
||||
|
||||
/// Encrypts a key via a password.
|
||||
fn encrypt_key(
|
||||
algorithm: &dyn CryptoAlgo,
|
||||
key: &[u8],
|
||||
password: &str,
|
||||
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
Ok(algorithm.encrypt(&key_by_password(algorithm, password), key))
|
||||
}
|
||||
|
||||
/// Decrypts a encrypted key via a password.
|
||||
fn decrypt_key(
|
||||
algorithm: &dyn CryptoAlgo,
|
||||
encrypted_key: &[u8],
|
||||
password: &str,
|
||||
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
algorithm.decrypt(&key_by_password(algorithm, password), encrypted_key)
|
||||
}
|
||||
|
||||
fn key_by_password(algorithm: &dyn CryptoAlgo, password: &str) -> Vec<u8> {
|
||||
let mut key = password.as_bytes().to_vec();
|
||||
key.resize(algorithm.key_len(), 0);
|
||||
key
|
||||
}
|
||||
|
||||
fn find_crypto(name: &str) -> Result<&dyn CryptoAlgo, Box<dyn std::error::Error>> {
|
||||
CRYPTO_ALGO_LIST
|
||||
.iter()
|
||||
.find(|x| x.name() == name)
|
||||
.copied()
|
||||
.ok_or_else(|| Box::from("unknown crypto algorithm"))
|
||||
}
|
||||
|
||||
trait CryptoAlgo: Send + Sync {
|
||||
/// Returns name of the algorithm.
|
||||
fn name(&self) -> &'static str;
|
||||
|
||||
/// Length of a key, in bytes.
|
||||
fn key_len(&self) -> usize;
|
||||
|
||||
/// Encrypt data.
|
||||
fn encrypt(&self, key: &[u8], data: &[u8]) -> Vec<u8>;
|
||||
|
||||
/// Decrypt data.
|
||||
fn decrypt(&self, key: &[u8], data: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>>;
|
||||
|
||||
/// Generates a key.
|
||||
fn generate_key(&self) -> Vec<u8> {
|
||||
let mut v = vec![0; self.key_len()];
|
||||
rand::fill(&mut v);
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Aes256Gcm;
|
||||
impl CryptoAlgo for Aes256Gcm {
|
||||
fn name(&self) -> &'static str {
|
||||
MasterKeyProvision::ALGORITHM_AES_256_GCM
|
||||
}
|
||||
|
||||
fn key_len(&self) -> usize {
|
||||
256 / 8
|
||||
}
|
||||
|
||||
fn encrypt(&self, key: &[u8], data: &[u8]) -> Vec<u8> {
|
||||
assert_eq!(key.len(), self.key_len(), "Invalid key length");
|
||||
|
||||
let mut nonce = [0u8; 12];
|
||||
rand::rng().fill_bytes(&mut nonce);
|
||||
let nonce = Nonce::from_slice(&nonce);
|
||||
|
||||
let cipher = AesGcm::new(GenericArray::from_slice(key));
|
||||
|
||||
let ciphertext = cipher
|
||||
.encrypt(nonce, data.as_ref())
|
||||
.expect("encryption failure!");
|
||||
|
||||
let mut result = Vec::with_capacity(nonce.len() + ciphertext.len());
|
||||
result.extend_from_slice(nonce);
|
||||
result.extend_from_slice(&ciphertext);
|
||||
result
|
||||
}
|
||||
|
||||
fn decrypt(&self, key: &[u8], data: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||
assert_eq!(key.len(), self.key_len(), "Invalid key length");
|
||||
|
||||
if data.len() < 12 {
|
||||
return Err("Data too short".into());
|
||||
}
|
||||
|
||||
let (nonce_bytes, ciphertext) = data.split_at(12);
|
||||
let nonce = Nonce::from_slice(nonce_bytes);
|
||||
|
||||
let cipher = AesGcm::new(GenericArray::from_slice(key));
|
||||
|
||||
let plaintext = cipher
|
||||
.decrypt(nonce, ciphertext)
|
||||
.map_err(|e| format!("decryption failure: {:?}", e))?;
|
||||
|
||||
Ok(plaintext)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
use auth_method_fx::Mode;
|
||||
use semios_account::{
|
||||
auth::{AuthenticateArgs, UpdateArgs},
|
||||
wellknown::OP_MODE_CLI,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CliMode;
|
||||
impl Mode for CliMode {
|
||||
fn name(&self) -> &'static str {
|
||||
OP_MODE_CLI
|
||||
}
|
||||
|
||||
fn authenticate(&self, args: AuthenticateArgs) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let password: String = dialoguer::Password::new()
|
||||
.allow_empty_password(true)
|
||||
.with_prompt("Password")
|
||||
.interact()?;
|
||||
if !matches!(crate::backend::verify(&args.user_uuid, &password), Ok(true)) {
|
||||
eprintln!("Sorry.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update(&self, args: UpdateArgs) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let old_password: String = dialoguer::Password::new()
|
||||
.allow_empty_password(true)
|
||||
.with_prompt("Old Password")
|
||||
.interact()?;
|
||||
let new_password: String = dialoguer::Password::new()
|
||||
.allow_empty_password(true)
|
||||
.with_prompt("New Password")
|
||||
.with_confirmation("Confirm password", "Passwords mismatching")
|
||||
.interact()?;
|
||||
if let Err(err) = crate::backend::update(&args.user_uuid, &old_password, &new_password) {
|
||||
eprintln!("Unable to update user password: {err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
mod api;
|
||||
mod backend;
|
||||
mod cli;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
auth_method_fx::App::new("password")
|
||||
.provides_master_key()
|
||||
.mode(Box::new(api::ApiMode))
|
||||
.mode(Box::new(cli::CliMode))
|
||||
.run()
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
[service]
|
||||
display-name = "System Account Service"
|
||||
description = "Service of system account information, authentication and federation."
|
||||
|
||||
[exec]
|
||||
start = "accountd"
|
||||
|
||||
[env]
|
||||
working_dir = "/"
|
||||
clear_vars = true
|
||||
Reference in New Issue
Block a user