use otter::crates::*;
use otter_support::imports::*;
use super::*;
use otter::commands::*;
use authproofs::*;
pub const IDLE_TIMEOUT: Duration = Duration::from_secs(60);
pub const UPLOAD_TIMEOUT: Duration = Duration::from_secs(60);
use pwd::Passwd;
use std::os::unix::io::AsRawFd;
use std::os::unix::net::UnixListener;
use uds::UnixStreamExt;
type CSE = anyhow::Error;
type TP = TablePermission;
use MgmtResponse::Fine;
const USERLIST: &str = "/etc/userlist";
const CREATE_PIECES_MAX: u32 = 300;
const OVERALL_PIECES_MAX: usize = 100_000;
const DEFAULT_POS_START: Pos = PosC::new(20,20);
const DEFAULT_POS_DELTA: Pos = PosC::new(5,5);
pub struct CommandListener {
listener: UnixListener,
}
struct CommandStream<'d> {
chan: MgmtChannel<TimedFdReader, TimedFdWriter>,
d: CommandStreamData<'d>,
}
struct CommandStreamData<'d> {
conn_desc: &'d str,
account: Option<AccountSpecified>,
authstate: AuthState,
}
impl Display for CommandStreamData<'_> {
#[throws(fmt::Error)]
fn fmt(&self, f: &mut Formatter) {
write!(f, "command conn {}", self.conn_desc)?;
if let Some(account) = &self.account {
write!(f, " {} ", &account.cooked)?;
}
}
}
type Euid = Result<Uid, ConnectionEuidDiscoverError>;
#[derive(Debug)]
enum AuthState {
None { euid: Euid },
Superuser { euid: Euid, auth: AuthorisationSuperuser },
Ssh { key: sshkeys::KeySpec, auth: Authorisation<sshkeys::KeySpec> },
}
#[derive(Debug,Clone)]
struct AccountSpecified {
notional_account: AccountName, cooked: String, auth: Authorisation<AccountName>, }
#[allow(clippy::enum_variant_names)]
enum PermissionCheckHow {
Instance,
InstanceOrOnlyAffectedAccount(AccountId),
InstanceOrOnlyAffectedPlayer(PlayerId),
}
type PCH = PermissionCheckHow;
pub const TP_ACCESS_BUNDLES: &[TP] = &[
TP::ViewNotSecret,
TP::Play,
TP::ChangePieces,
TP::UploadBundles,
];
fn execute_and_respond<W>(cs: &mut CommandStreamData, cmd: MgmtCommand,
bulk_upload: &mut ReadFrame<TimedFdReader>,
for_response: &mut FrameWriter<W>)
-> Result<(), CSE>
where W: Write
{
let mut bulk_download: Option<Box<dyn Read>> = None;
let mut for_response = for_response
.write_withbulk().context("start to respond")?;
let mut cmd_s = log_enabled!(log::Level::Info)
.as_some_from(|| format!("{:?}", &cmd))
.unwrap_or_default();
const MAX: usize = 200;
if cmd_s.len() > MAX-3 {
cmd_s.truncate(MAX-3);
cmd_s += "..";
}
#[throws(MgmtError)]
fn start_access_game(game: &InstanceName)
-> (AccountsGuard, Unauthorised<InstanceRef, InstanceName>)
{
(
AccountsGuard::lock(),
Instance::lookup_by_name_unauth(game)?,
)
}
#[throws(MgmtError)]
fn access_bundles<'ig,F,R>(
cs: &mut CommandStreamData,
ag: &AccountsGuard,
gref: &'ig Unauthorised<InstanceRef, InstanceName>,
perms: &[TablePermission],
f: &mut F,
) -> (R, Authorisation<InstanceName>)
where F: FnMut(
&mut InstanceGuard<'ig>,
BundlesGuard<'ig>,
) -> Result<R, MgmtError>
{
let bundles = gref.lock_bundles();
let mut igu = gref.lock()?;
let (ig, auth) = cs.check_acl(ag, &mut igu, PCH::Instance, perms)?;
let bundles = bundles.by(auth);
let r = f(ig, bundles)?;
(r, auth)
}
#[throws(MgmtError)]
fn start_access_ssh_keys(cs: &CommandStreamData)
-> (AccountsGuard, AccountId, Authorisation<AccountScope>)
{
let ag = AccountsGuard::lock();
let wanted = &cs.current_account()?.notional_account;
let acctid = ag.check(wanted)?;
let auth = authorise_scope_direct(cs, &ag, &wanted.scope)?;
(ag, acctid, auth)
}
let resp = (|| Ok::<_,MgmtError>(match cmd {
MC::Noop => Fine,
MC::SetSuperuser(enable) => {
let preserve_euid = match &cs.authstate {
AuthState::None { euid, .. } => euid,
AuthState::Superuser { euid, .. } => euid,
AuthState::Ssh { .. } => throw!(ME::AuthorisationError),
}.clone();
if !enable {
cs.authstate = AuthState::None { euid: preserve_euid };
} else {
let ag = AccountsGuard::lock();
let auth = authorise_scope_direct(cs, &ag, &AccountScope::Server)?;
let auth = auth.so_promise();
cs.authstate = AuthState::Superuser { euid: preserve_euid, auth };
}
Fine
},
MC::ThisConnAuthBy => {
use MgmtThisConnAuthBy as MTCAB;
MR::ThisConnAuthBy(match &cs.authstate {
AuthState::None { .. } => MTCAB::Local,
AuthState::Superuser { .. } => MTCAB::Local,
AuthState::Ssh { key, .. } => MTCAB::Ssh { key: key.clone() },
})
}
MC::SetRestrictedSshScope { key } => {
if cs.account.is_some() { throw!(ME::AccountSpecified) }
let good_uid = Some(config().ssh_proxy_uid);
let auth = cs.authorised_uid(good_uid, Some("SetRestrictedScope"))
.map_err(|_| ME::AuthorisationError)?;
let auth = auth.so_promise();
cs.authstate = AuthState::Ssh { key, auth };
Fine
},
MC::CreateAccount(AccountDetails {
account, nick, timezone, access, layout
}) => {
let mut ag = AccountsGuard::lock();
let auth = authorise_for_account(cs, &ag, &account)?;
let access = cs.accountrecord_from_spec(access)?
.unwrap_or_else(|| AccessRecord::new_unset());
let nick = nick.unwrap_or_else(|| account.to_string());
let account = account.into();
let layout = layout.unwrap_or_default();
let record = AccountRecord {
account, nick, access,
layout,
timezone: timezone.unwrap_or_default(),
ssh_keys: default(),
};
ag.insert_entry(record, auth)?;
Fine
}
MC::UpdateAccount(AccountDetails {
account, nick, timezone, access, layout
}) => {
let mut ag = AccountsGuard::lock();
let mut games = games_lock();
let auth = authorise_for_account(cs, &ag, &account)?;
let access = cs.accountrecord_from_spec(access)?;
ag.with_entry_mut(&mut games, &account, auth, access, |record, _acctid|{
fn update_from<T>(spec: Option<T>, record: &mut T) {
if let Some(new) = spec { *record = new; }
}
update_from(nick, &mut record.nick );
update_from(timezone, &mut record.timezone);
update_from(layout, &mut record.layout );
Fine
})
?
.map_err(|(e,_)|e) ?
}
MC::DeleteAccount(account) => {
let mut ag = AccountsGuard::lock();
let mut games = games_lock();
let auth = authorise_for_account(cs, &ag, &account)?;
ag.remove_entry(&mut games, &account, auth)?;
Fine
}
MC::SelectAccount(wanted_account) => {
let ag = AccountsGuard::lock();
let auth = authorise_scope_direct(cs, &ag, &wanted_account.scope)?;
cs.account = Some(AccountSpecified {
cooked: wanted_account.to_string(),
notional_account: wanted_account,
auth: auth.so_promise(),
});
Fine
}
MC::CheckAccount => {
let ag = AccountsGuard::lock();
let _ok = ag.lookup(&cs.current_account()?.notional_account)?;
Fine
}
MC::ListAccounts { all } => {
let ag = AccountsGuard::lock();
let names = if all == Some(true) {
let auth = cs.superuser().ok_or(ME::AuthorisationError)?;
ag.list_accounts_all(auth)
} else {
let AccountSpecified { notional_account, auth, .. } =
cs.account.as_ref().ok_or(ME::SpecifyAccount)?;
ag.list_accounts_scope(¬ional_account.scope, *auth)
};
MR::AccountsList(names)
}
MC::CreateGame { game, insns } => {
let mut ag = AccountsGuard::lock();
let mut games = games_lock();
let auth = authorise_by_account(cs, &ag, &game)?;
let gs = otter::gamestate::GameState {
table_colour: Html::lit("green").into(),
table_size: DEFAULT_TABLE_SIZE,
pieces: default(),
players: default(),
log: default(),
gen: Generation(0),
max_z: ZLevel::zero(),
occults: default(),
};
let acl = default();
let gref = Instance::new(game, gs, &mut games, acl, auth)?;
let ig = gref.lock()?;
let mut ig = Unauthorised::of(ig);
let resp =
execute_for_game(cs, &mut ag, &mut ig,
insns, MgmtGameUpdateMode::Bulk)
.map_err(|e|{
let ig = ig.by(Authorisation::promise_any());
let name = ig.name.clone();
let InstanceGuard { c, .. } = ig;
Instance::destroy_game(&mut games, c, auth)
.unwrap_or_else(|e| warn!(
"failed to tidy up failecd creation of {:?}: {:?}",
&name, &e
));
e
})?;
resp
}
MC::UploadBundle { game, size, hash, kind, progress } => {
let (upload, auth) = {
let (ag, gref) = start_access_game(&game)?;
access_bundles(
cs,&ag,&gref, &[TP::UploadBundles],
&mut |ig, mut bundles: BundlesGuard<'_>| {
bundles.start_upload(ig, kind)
}
)?
};
bulk_upload.inner_mut().set_timeout(Some(UPLOAD_TIMEOUT));
let uploaded = upload.bulk(bulk_upload, size,
&hash, progress, &mut for_response)?;
let bundle = {
let gref = Instance::lookup_by_name(&game, auth)?;
let mut bundles = gref.lock_bundles();
let mut ig = gref.lock()?;
bundles.finish_upload(&mut ig, uploaded)?
};
MR::Bundle { bundle }
}
MC::ListBundles { game } => {
let (ag, gref) = start_access_game(&game)?;
let (bundles,_auth) =
access_bundles(
cs,&ag,&gref,TP_ACCESS_BUNDLES,
&mut |ig,_|{
Ok(ig.bundle_list.clone())
})?;
MR::Bundles { bundles }
}
MC::DownloadBundle { game, id } => {
let (ag, gref) = start_access_game(&game)?;
access_bundles(
cs,&ag,&gref,TP_ACCESS_BUNDLES,
&mut |ig,_|{
let f = id.open(ig)?.ok_or_else(|| ME::BundleNotFound)?;
bulk_download = Some(Box::new(f));
Ok(())
})?;
Fine
}
MC::ClearBundles { game } => {
let (ag, gref) = start_access_game(&game)?;
access_bundles(
cs,&ag,&gref, &[TP::ClearBundles],
&mut |ig, mut bundles: BundlesGuard<'_>| {
bundles.clear(ig)
})?;
Fine
}
MC::ListGames { all } => {
let ag = AccountsGuard::lock();
let names = Instance::list_names(
None, Authorisation::promise_any());
let auth_all = if all == Some(true) {
let auth = cs.superuser().ok_or(ME::AuthorisationError)?.into();
Some(auth)
} else {
None
};
let mut names = names.into_iter().map(|name| {
let gref = Instance::lookup_by_name_unauth(&name)?;
let mut igu = gref.lock_even_destroying();
let _ig = if let Some(auth_all) = auth_all {
igu.by_ref(auth_all)
} else {
cs.check_acl(&ag, &mut igu, PCH::Instance, &[TP::ShowInList])?.0
};
Ok::<_,ME>(name)
}).filter(|ent| matches_doesnot!(
ent,
= Ok(_),
! Err(ME::GameNotFound) | Err(ME::AuthorisationError),
= Err(_),
))
.collect::<Result<Vec<_>,_>>() ?;
names.sort_unstable();
MR::GamesList(names)
}
MC::AlterGame { game, insns, how } => {
let (mut ag, gref) = start_access_game(&game)?;
let mut g = gref.lock()?;
execute_for_game(cs, &mut ag, &mut g, insns, how)?
}
MC::DestroyGame { game } => {
let ag = AccountsGuard::lock();
let mut games = games_lock();
let auth = authorise_by_account(cs, &ag, &game)?;
let gref = Instance::lookup_by_name_locked(&games, &game, auth)?;
let ig = gref.lock_even_destroying();
Instance::destroy_game(&mut games, ig, auth)?;
Fine
}
MC::LibraryListLibraries { game } => {
let (ag, gref) = start_access_game(&game)?;
let (libs, _auth) =
access_bundles(
cs,&ag,&gref, &[TP::UploadBundles],
&mut |ig, _| Ok(
ig.all_shapelibs()
.iter()
.map(|reg| reg.iter())
.flatten()
.map(|ll| ll.iter())
.flatten()
.map(|l| l.enquiry())
.collect::<Vec<LibraryEnquiryData>>()
)
)?;
MR::Libraries(libs)
}
MC::LibraryListByGlob { game, lib, pat } => {
let (ag, gref) = start_access_game(&game)?;
let (results, _auth) =
access_bundles(
cs,&ag,&gref, &[TP::UploadBundles],
&mut |ig, _| {
let regs = ig.all_shapelibs();
let mut results: Vec<ItemEnquiryData> = default();
let libss = if let Some(lib) = &lib {
vec![regs.lib_name_lookup(lib)?]
} else {
regs.all_libs().collect()
};
for libs in libss {
for lib in libs {
results.extend(lib.list_glob(&pat)?);
}
}
Ok(results)
})?;
MR::LibraryItems(results)
}
MC::SshListKeys => {
let (ag, acctid, auth) = start_access_ssh_keys(cs)?;
let list = ag.sshkeys_report(acctid, auth)?;
MR::SshKeys(list)
}
MC::SshAddKey { akl } => {
let (mut ag, acctid, auth) = start_access_ssh_keys(cs)?;
let (index, id) = ag.sshkeys_add(acctid, akl, auth)?;
MR::SshKeyAdded { index, id }
}
MC::SshDeleteKey { index, id } => {
let (mut ag, acctid, auth) = start_access_ssh_keys(cs)?;
ag.sshkeys_remove(acctid, index, id, auth)?;
MR::Fine
}
MC::SshReinstallKeys => {
let superuser = cs.superuser()
.ok_or(ME::SuperuserAuthorisationRequired)?;
let mut ag = AccountsGuard::lock();
ag.sshkeys_rewrite_authorized_keys(superuser)?;
MR::Fine
}
MC::LoadFakeRng(ents) => {
let superuser = cs.superuser()
.ok_or(ME::SuperuserAuthorisationRequired)?;
config().game_rng.set_fake(ents, superuser)?;
Fine
}
MC::SetFakeTime(fspec) => {
let superuser = cs.superuser()
.ok_or(ME::SuperuserAuthorisationRequired)?;
config().global_clock.set_fake(fspec, superuser)?;
Fine
}
}))();
let resp = match resp {
Ok(resp) => {
info!("{}: executed {}", &cs, cmd_s);
resp
}
Err(error) => {
info!("{}: error {:?} from {}", &cs, &error, cmd_s);
MgmtResponse::Error { error }
}
};
let mut wf = for_response.respond(&resp).context("respond")?;
if let Some(mut bulk_download) = bulk_download {
io::copy(&mut bulk_download, &mut wf).context("download")?;
}
wf.finish().context("flush")?;
Ok(())
}
type ExecuteGameInsnResults<'igr, 'ig> = (
ExecuteGameChangeUpdates,
MgmtGameResponse,
UnpreparedUpdates, Vec<MgmtGameInstruction>,
&'igr mut InstanceGuard<'ig>,
);
fn execute_game_insn<'cs, 'igr, 'ig: 'igr>(
cs: &'cs CommandStreamData,
ag: &'_ mut AccountsGuard,
ig: &'igr mut Unauthorised<InstanceGuard<'ig>, InstanceName>,
update: MgmtGameInstruction,
who: &Html,
to_permute: &mut ToRecalculate,
)
-> Result<ExecuteGameInsnResults<'igr, 'ig> ,ME>
{
type U = ExecuteGameChangeUpdates;
use MgmtGameResponse::Fine;
fn tz_from_str(s: &str) -> Timezone {
Timezone::from_str(s).void_unwrap()
}
fn no_updates<'igr,'ig>(ig: &'igr mut InstanceGuard<'ig>,
mgr: MgmtGameResponse)
-> ExecuteGameInsnResults<'igr, 'ig> {
(U{ pcs: vec![], log: vec![], raw: None }, mgr, default(), vec![], ig)
}
#[throws(MgmtError)]
fn readonly<'igr, 'ig: 'igr, 'cs,
F: FnOnce(&InstanceGuard) -> Result<MgmtGameResponse,ME>,
P: Into<PermSet<TablePermission>>>
(
cs: &'cs CommandStreamData,
ag: &AccountsGuard,
ig: &'igr mut Unauthorised<InstanceGuard<'ig>, InstanceName>,
p: P,
f: F
) -> ExecuteGameInsnResults<'igr, 'ig>
{
let (ig, _) = cs.check_acl(ag, ig, PCH::Instance, p)?;
let resp = f(ig)?;
(U{ pcs: vec![], log: vec![], raw: None }, resp, default(), vec![], ig)
}
#[throws(MgmtError)]
fn pieceid_lookup<'igr, 'ig: 'igr, 'cs,
F: FnOnce(&PerPlayerIdMap) -> MGR>
(
cs: &'cs CommandStreamData,
ig: &'igr mut Unauthorised<InstanceGuard<'ig>, InstanceName>,
player: PlayerId,
f: F,
) -> ExecuteGameInsnResults<'igr, 'ig>
{
let superuser = cs.superuser().ok_or(ME::SuperuserAuthorisationRequired)?;
let ig = ig.by_mut(superuser.into());
let gpl = ig.gs.players.byid(player)?;
let resp = f(&gpl.idmap);
no_updates(ig, resp)
}
#[throws(MgmtError)]
fn some_synch_core(ig: &mut InstanceGuard<'_>) -> (Generation, MGR) {
let mut buf = PrepareUpdatesBuffer::new(ig, None);
let gen = buf.gen();
drop(buf); ig.save_game_now()?;
(gen, MGR::Synch(gen))
}
#[throws(MgmtError)]
fn update_links<'igr, 'ig: 'igr, 'cs,
F: FnOnce(&mut Arc<LinksTable>) -> Result<Html,ME>>
(
cs: &'cs CommandStreamData,
ag: &'_ mut AccountsGuard,
ig: &'igr mut Unauthorised<InstanceGuard<'ig>, InstanceName>,
f: F
) -> ExecuteGameInsnResults<'igr, 'ig>
{
let ig = cs.check_acl(ag, ig, PCH::Instance, &[TP::SetLinks])?.0;
let log_html = f(&mut ig.links)?;
let log = vec![LogEntry { html: log_html, }];
(U{ log,
pcs: vec![],
raw: Some(vec![ PreparedUpdateEntry::SetLinks(ig.links.clone()) ])},
Fine, default(), vec![], ig)
}
#[throws(MgmtError)]
fn reset_game<'igr, 'ig: 'igr, 'cs>(
cs: &'cs CommandStreamData,
ag: &'_ mut AccountsGuard,
ig: &'igr mut Unauthorised<InstanceGuard<'ig>, InstanceName>,
f: Box<dyn FnOnce(&mut InstanceGuard, &mut Vec<MgmtGameInstruction>)
-> Result<LogEntry, MgmtError> + '_>
) -> ExecuteGameInsnResults<'igr, 'ig>
{
let ig = cs.check_acl(ag, ig, PCH::Instance, &[TP::ChangePieces])?.0;
let mut insns = vec![];
for alias in ig.pcaliases.keys() {
insns.push(MGI::DeletePieceAlias(alias.clone()));
}
for piece in ig.gs.pieces.keys() {
insns.push(MGI::DeletePiece(piece));
}
let logent = f(ig, &mut insns)?;
(U{ pcs: vec![],
log: vec![ logent ],
raw: None },
MGR::InsnExpanded,
default(), insns, ig)
}
#[throws(MgmtError)]
fn reset_game_from_spec<'igr, 'ig: 'igr, 'cs>(
cs: &'cs CommandStreamData,
ag: &'_ mut AccountsGuard,
ig: &'igr mut Unauthorised<InstanceGuard<'ig>, InstanceName>,
who: &'_ Html,
get_spec_toml: Box<dyn FnOnce(&InstanceGuard)
-> Result<String, MgmtError>>,
) -> ExecuteGameInsnResults<'igr, 'ig>
{
reset_game(cs,ag,ig, Box::new(|ig, insns|{
let spec = get_spec_toml(ig)?;
let spec: toml::Value = spec.parse()
.map_err(|e: toml::de::Error| ME::TomlSyntaxError(e.to_string()))?;
let GameSpec {
pieces, table_size, table_colour, pcaliases,
mformat: _,
} = toml_de::from_value(&spec)
.map_err(|e: toml_de::Error| ME::TomlStructureError(e.to_string()))?;
for (alias, target) in pcaliases.into_iter() {
insns.push(MGI::DefinePieceAlias{ alias, target });
}
insns.push(MGI::ClearLog);
insns.push(MGI::SetTableSize(table_size));
insns.push(MGI::SetTableColour(table_colour));
for pspec in pieces.into_iter() {
insns.push(MGI::AddPieces(pspec));
}
let html = hformat!("{} reset the game", &who);
Ok(LogEntry { html })
}))?
}
impl<'cs> CommandStreamData<'cs> {
#[throws(MgmtError)]
fn check_acl_manip_player_access<'igr, 'ig: 'igr>(
&self,
ag: &AccountsGuard,
ig: &'igr mut Unauthorised<InstanceGuard<'ig>, InstanceName>,
player: PlayerId,
perm: TablePermission,
) -> (&'igr mut InstanceGuard<'ig>, Authorisation<AccountName>) {
let (ig, auth) = self.check_acl(ag, ig,
PCH::InstanceOrOnlyAffectedPlayer(player),
&[perm])?;
fn auth_map(n: &InstanceName) -> &AccountName { &n.account }
let auth = auth.map(auth_map);
(ig, auth)
}
}
let y = match update {
MGI::Noop { } => readonly(cs,ag,ig, &[TP::TestExistence], |_| Ok(Fine))?,
MGI::SetTableSize(size) => {
let ig = cs.check_acl(ag, ig, PCH::Instance, &[TP::ChangePieces])?.0;
for p in ig.gs.pieces.values() {
p.pos.clamped(size)?;
}
ig.gs.table_size = size;
(U{ pcs: vec![],
log: vec![ LogEntry {
html: hformat!("{} resized the table to {}x{}",
who, size.x(), size.y()),
}],
raw: Some(vec![ PreparedUpdateEntry::SetTableSize(size) ]) },
Fine, default(), vec![], ig)
}
MGI::SetTableColour(colour) => {
let ig = cs.check_acl(ag, ig, PCH::Instance, &[TP::ChangePieces])?.0;
let colour: Colour = (&colour).try_into().map_err(|e| SpE::from(e))?;
ig.gs.table_colour = colour.clone();
(U{ pcs: vec![],
log: vec![ LogEntry {
html: hformat!("{} recoloured the tabletop to {}",
&who, &colour),
}],
raw: Some(vec![ PreparedUpdateEntry::SetTableColour(colour) ]) },
Fine, default(), vec![], ig)
}
MGI::JoinGame {
details: MgmtPlayerDetails { nick },
} => {
let account = &cs.current_account()?.notional_account;
let (arecord, acctid) = ag.lookup(account)?;
let (ig, auth) = cs.check_acl(ag, ig, PCH::Instance, &[TP::Play])?;
let nick = nick.ok_or(ME::MustSpecifyNick)?;
let logentry = LogEntry {
html: hformat!("{} [{}] joined the game", nick, account),
};
let timezone = &arecord.timezone;
let tz = tz_from_str(timezone);
let gpl = GPlayer {
nick: nick.to_string(),
layout: arecord.layout,
idmap: default(),
moveheld: default(),
movehist: default(),
};
let ipl = IPlayer {
acctid,
tz,
tokens_revealed: default(),
};
let (player, update, logentry) =
ig.player_new(gpl, ipl, arecord.account.clone(), logentry)?;
let atr = ig.player_access_reset(ag, player, auth.so_promise())?;
(U{ pcs: vec![],
log: vec![ logentry ],
raw: Some(vec![ update ] )},
MGR::JoinGame { nick, player, token: atr },
default(), vec![], ig)
},
MGI::DeletePieceAlias(alias) => {
let ig = cs.check_acl(ag, ig, PCH::Instance, &[TP::ChangePieces])?.0;
ig.pcaliases.remove(&alias);
no_updates(ig, MGR::Fine)
},
MGI::DefinePieceAlias { alias, target } => {
let ig = cs.check_acl(ag, ig, PCH::Instance, &[TP::ChangePieces])?.0;
ig.pcaliases.insert(alias, target);
no_updates(ig, MGR::Fine)
},
MGI::ClearGame { } => {
reset_game(cs,ag,ig, Box::new(|_ig, _insns|{
let html = hformat!("{} cleared out the game", &who);
Ok(LogEntry { html })
}))?
}
MGI::ResetFromNamedSpec { spec } => {
reset_game_from_spec(cs,ag,ig,who, Box::new(move |ig| {
let data = bundles::load_spec_to_read(ig,&spec)?;
Ok::<_,ME>(data)
}))?
}
MGI::ResetFromGameSpec { spec_toml: spec } => {
reset_game_from_spec(cs,ag,ig,who, Box::new(|_| Ok::<_,ME>(spec)))?
}
MGI::InsnMark(token) => {
let (ig, _) = cs.check_acl(ag,ig,PCH::Instance, &[TP::TestExistence])?;
no_updates(ig, MGR::InsnMark(token))
}
MGI::Synch => {
let (ig, _) = cs.check_acl(ag, ig, PCH::Instance, &[TP::Play])?;
let (_gen, mgr) = some_synch_core(ig)?;
no_updates(ig, mgr)
}
MGI::SynchLog => {
let superuser = cs.superuser()
.ok_or(ME::SuperuserAuthorisationRequired)?;
let ig = ig.by_mut(superuser.into());
let (gen, mgr) = some_synch_core(ig)?;
let log = LogEntry { html: synch_logentry(gen) };
(U{ pcs: vec![], log: vec![log], raw: None }, mgr, default(), vec![], ig)
},
MGI::PieceIdLookupFwd { player, piece } => {
pieceid_lookup(
cs, ig, player,
|ppidm| MGR::VisiblePieceId(ppidm.fwd(piece))
)?
},
MGI::PieceIdLookupRev { player, vpid } => {
pieceid_lookup(
cs, ig, player,
|ppidm| MGR::InternalPieceId(ppidm.rev(vpid))
)?
},
MGI::ListPieces => readonly(cs,ag,ig, &[TP::ViewNotSecret], |ig|{
let ioccults = &ig.ioccults;
let pieces = ig.gs.pieces.iter().filter_map(
|(piece,gpc)| (|| Ok::<_,MgmtError>(if_chain!{
let &GPiece { pos, face, .. } = gpc;
if let Some(ipc) = ig.ipieces.get(piece);
let unocc = gpc.fully_visible_to_everyone();
let visible = if let Some(y) = unocc {
let pri = PieceRenderInstructions::new_visible(
VisiblePieceId(piece.data())
);
let bbox = ipc.show(y).bbox_approx()?;
let desc_html = pri.describe(ioccults,&ig.gs.occults, gpc, ipc);
Some(MgmtGamePieceVisibleInfo {
pos, face, desc_html, bbox
})
} else {
None
};
let itemname = if let Some(unocc) = unocc {
ipc.show(unocc).itemname().to_string()
} else {
"occulted-item".to_string()
};
let itemname = itemname.try_into().map_err(
|e| internal_error_bydebug(&e)
)?;
then {
Some(MgmtGamePieceInfo {
piece,
itemname,
visible,
})
} else {
None
}
}))().transpose()
).collect::<Result<Vec<_>,_>>()?;
let pcaliases = ig.pcaliases.keys().cloned().collect();
Ok(MGR::Pieces { pieces, pcaliases })
})?,
MGI::UpdatePlayer {
player,
details: MgmtPlayerDetails { nick },
} => {
let ig = cs.check_acl_modify_player(ag, ig, player,
&[TP::ModifyOtherPlayer])?.0;
let mut log = vec![];
if let Some(new_nick) = nick {
ig.check_new_nick(&new_nick)?;
let gpl = ig.gs.players.byid_mut(player)?;
log.push(LogEntry {
html: hformat!("{} changed {}'s nick to {}",
&who, gpl.nick, new_nick),
});
gpl.nick = new_nick;
}
let update = ig.prepare_set_player_update(player)?;
(U{ log,
pcs: vec![],
raw: Some(vec![update])},
Fine, default(), vec![], ig)
},
MGI::Info => readonly(cs,ag,ig, &[TP::ViewNotSecret], |ig|{
let players = ig.gs.players.iter().map(
|(player, gpl)| {
let nick = gpl.nick.clone();
if_chain! {
if let Some(ipr) = ig.iplayers.get(player);
let ipl = &ipr.ipl;
if let Ok((arecord, _)) = ag.lookup(ipl.acctid);
then { Ok((player, MgmtPlayerInfo {
account: (*arecord.account).clone(),
nick,
})) }
else {
throw!(InternalError::PartialPlayerData)
}
}
}
).collect::<Result<SecondarySlotMap<_,_>,ME>>()?;
let table_size = ig.gs.table_size;
let links = ig.links.iter().filter_map(
|(k,v)|
Some((k.clone(), UrlSpec(v.as_ref()?.clone())))
).collect();
let info = MgmtGameResponseGameInfo { table_size, players, links };
Ok(MGR::Info(info))
})?,
MGI::SetLinks(mut spec_links) => {
update_links(cs,ag,ig, |ig_links|{
let mut new_links: LinksTable = default();
for (k,v) in spec_links.drain() {
let url: Url = (&v).try_into()?;
new_links[k] = Some(url.into());
}
let new_links = Arc::new(new_links);
*ig_links = new_links;
Ok(
hformat!("{} set the links to off-server game resources",
who)
)
})?
}
MGI::SetLink { kind, url } => {
update_links(cs,ag,ig, |ig_links|{
let mut new_links: LinksTable = (**ig_links).clone();
let url: Url = (&url).try_into()?;
let show: Html = (kind, url.as_str()).to_html();
new_links[kind] = Some(url.into());
let new_links = Arc::new(new_links);
*ig_links = new_links;
Ok(
hformat!("{} set the link {}",
who, show)
)
})?
}
MGI::RemoveLink { kind } => {
update_links(cs,ag,ig, |ig_links|{
let mut new_links: LinksTable = (**ig_links).clone();
new_links[kind] = None;
let new_links = Arc::new(new_links);
*ig_links = new_links;
Ok(
hformat!("{} removed the link {}",
who, &kind)
)
})?
}
MGI::ResetPlayerAccess(player) => {
let (ig, auth) = cs.check_acl_manip_player_access
(ag, ig, player, TP::ResetOthersAccess)?;
let token = ig.player_access_reset(ag, player, auth)?;
no_updates(ig, MGR::PlayerAccessToken(token))
}
MGI::RedeliverPlayerAccess(player) => {
let (ig, auth) = cs.check_acl_manip_player_access
(ag, ig, player, TP::RedeliverOthersAccess)?;
let token = ig.player_access_redeliver(ag, player, auth)?;
no_updates(ig, MGR::PlayerAccessToken(token))
},
MGI::LeaveGame(player) => {
let account = &cs.current_account()?.notional_account;
let (ig, _auth) = cs.check_acl_manip_player_access
(ag, ig, player, TP::ModifyOtherPlayer)?;
let got = ig.players_remove(&[player].iter().cloned().collect())?;
let (gpl, ipl, update) = got.into_iter().next()
.ok_or(PlayerNotFound)?;
let html =
hformat!("{} [{}] left the game [{}]"
,
(|| Some(gpl?.nick))()
.unwrap_or_else(|| "<partial data!>".into())
,
(||{
let (record, _) = ag.lookup(ipl?.acctid).ok()?;
Some(record.account.to_string())
})()
.unwrap_or_else(|| "<account deleted>".into())
,
&account
)
;
(U{ pcs: vec![],
log: vec![ LogEntry { html }],
raw: Some(vec![ update ]) },
Fine, default(), vec![], ig)
},
MGI::DeletePiece(piece) => {
let (ig, modperm, _) = cs.check_acl_modify_pieces(ag, ig)?;
let _ipc = ig.ipieces.as_mut(modperm)
.get(piece).ok_or(ME::PieceNotFound)?;
let (desc_html, puo, xupdates) =
ig.delete_piece(modperm, to_permute, piece,
|ioccults, goccults, ipc, gpc| {
if let (Some(ipc), Some(gpc)) = (ipc, gpc) {
let pri = PieceRenderInstructions::new_visible(default());
pri.describe(ioccults,goccults, gpc, ipc)
} else {
"<piece partially missing from game state>".to_html()
}
})?;
(U{ pcs: vec![(piece, puo)],
log: vec![ LogEntry {
html: hformat!("A piece {} was removed from the game",
desc_html),
}],
raw: None },
Fine,
xupdates,
vec![],
ig)
},
MGI::AddPieces(PiecesSpec{ pos,posd,count,face,pinned,angle,info }) => {
let (ig_g, modperm, _) = cs.check_acl_modify_pieces(ag, ig)?;
let ig = &mut **ig_g;
let gs = &mut ig.gs;
let implicit: u32 = info.count(&ig.pcaliases)?
.try_into().map_err(
|_| SpE::InternalError(format!("implicit item count out of range"))
)?;
let angle = angle.resolve()?;
let count: Box<dyn ExactSizeIterator<Item=u32>> = match count {
Some(explicit) if implicit == 1 => {
Box::new((0..explicit).map(|_| 0))
},
Some(explicit) if implicit != explicit => {
throw!(SpecError::InconsistentPieceCount)
},
None | Some(_) => {
Box::new(0..implicit)
},
};
let count_len = count.len();
if count_len > CREATE_PIECES_MAX as usize { throw!(ME::LimitExceeded) }
if gs.pieces.len() + count_len > OVERALL_PIECES_MAX {
throw!(ME::LimitExceeded)
}
let posd = posd.unwrap_or(DEFAULT_POS_DELTA);
let mut updates = Vec::with_capacity(count_len);
let mut pos = pos.unwrap_or(DEFAULT_POS_START);
let mut z = gs.max_z.z.clone_mut();
for piece_i in count {
let gs = &mut ig.gs;
let face = face.unwrap_or_default();
let mut gpc = GPiece {
held: None,
zlevel: ZLevel { z: z.increment()?, zg: gs.gen },
lastclient: default(),
occult: default(),
gen_before_lastclient: Generation(0),
pinned: pinned.unwrap_or(false),
angle,
gen: gs.gen,
pos, face,
xdata: None,
moveable: default(),
last_released: default(),
rotateable: true,
fastsplit: None,
};
let SpecLoaded { p, occultable, special } =
info.load(PLA {
i: piece_i as usize,
gpc: &mut gpc,
ig,
depth: SpecDepth::zero(),
})?;
if p.nfaces() <= face.into() {
throw!(SpecError::FaceNotFound);
}
let gs = &mut ig.gs;
gpc.pos.clamped(gs.table_size)?;
if gpc.zlevel > gs.max_z { gs.max_z = gpc.zlevel.clone() }
let piece = gs.pieces.as_mut(modperm).insert(gpc);
let gpc = gs.pieces.get_mut(piece).ok_or_else(
|| internal_logic_error("just inserted but now missing!"))?;
let p = IPieceTraitObj::new(p);
(||{
let ilks = &mut ig.ioccults.ilks;
let occilk = occultable.map(|(lilk, p_occ)| {
let data = OccultIlkData { p_occ };
ilks.load_lilk(lilk, data)
});
let mut ipc = IPiece { p, occilk, special };
if let Some(fsid) = &mut gpc.fastsplit {
(ipc, *fsid) = ig.ifastsplits.new_original(ilks, ipc);
}
ig.ipieces.as_mut(modperm).insert(piece, ipc);
updates.push((piece, PieceUpdateOp::InsertQuiet(())));
})(); pos = (pos + posd)?;
}
(U{ pcs: updates,
log: vec![ LogEntry {
html: hformat!("{} added {} pieces", who, count_len),
}],
raw: None },
Fine, default(), vec![], ig_g)
},
MGI::ClearLog => {
let (ig, _) = cs.check_acl(ag, ig, PCH::Instance, &[TP::Super])?;
for gpl in ig.gs.players.values_mut() {
gpl.movehist.clear();
}
ig.gs.log.clear();
for ipr in ig.iplayers.values_mut() {
let tr = &mut ipr.ipl.tokens_revealed;
let latest = tr.values()
.map(|trv| trv.latest)
.max();
if let Some(latest) = latest {
tr.retain(|_k, v| v.latest >= latest);
}
}
let raw = Some(vec![ PUE::MoveHistClear ]);
(U{ pcs: vec![ ],
log: vec![ LogEntry {
html: hformat!("{} cleared the log history", who),
} ],
raw },
Fine, default(), vec![], ig)
},
MGI::SetACL { acl } => {
let (ig, _) = cs.check_acl(ag, ig, PCH::Instance, &[TP::Super])?;
ig.acl = acl.into();
let mut log = vec![ LogEntry {
html: hformat!("{} set the table access control list",
who),
} ];
#[throws(InternalError)]
fn remove_old_players(ag: &AccountsGuard, ig: &mut InstanceGuard,
who: &Html,
log: &mut Vec<LogEntry>)
-> Vec<PreparedUpdateEntry>
{
let owner_account = ig.name.account.to_string();
let eacl = EffectiveACL {
owner_account: Some(&owner_account),
acl: &ig.acl,
};
let mut remove = HashSet::new();
for (player, ipr) in &ig.iplayers {
if_chain! {
let acctid = ipr.ipl.acctid;
if let Ok((record,_)) = ag.lookup(acctid);
let perm = &[TP::Play];
if let Ok(_) = eacl.check(&record.account.to_string(),
perm.into());
then {
}
else {
remove.insert(player);
}
};
};
let mut updates = Vec::new();
for (gpl, _ipl, update) in ig.players_remove(&remove)? {
let show = if let Some(gpl) = gpl {
htmlescape::encode_minimal(&gpl.nick)
} else {
"<i>partial data?!</i>".to_string()
};
log.push(LogEntry {
html: hformat!("{} removed a player {}", who, show),
});
updates.push(update);
}
updates
}
let updates = remove_old_players(ag, ig, who, &mut log)?;
(U{ pcs: vec![ ],
log,
raw: Some(updates) },
Fine, default(), vec![], ig)
},
};
Ok(y)
}
#[throws(ME)]
fn execute_for_game<'cs, 'igr, 'ig: 'igr>(
cs: &'cs CommandStreamData,
ag: &mut AccountsGuard,
igu: &'igr mut Unauthorised<InstanceGuard<'ig>, InstanceName>,
insns: Vec<MgmtGameInstruction>,
how: MgmtGameUpdateMode) -> MgmtResponse
{
let (ok, uu) = ToRecalculate::with(|mut to_permute| {
let r = (||{
let who = if_chain! {
let account = &cs.current_account()?.notional_account;
let ig = igu.by_ref(Authorisation::promise_any());
if let Ok((_, acctid)) = ag.lookup(account);
if let Some((player,_)) = ig.iplayers.iter()
.find(|(_,ipr)| ipr.ipl.acctid == acctid);
if let Some(gpl) = ig.gs.players.get(player);
then { hformat!("{} [{}]",
gpl.nick,
account) }
else { hformat!("[{}]",
account) }
};
let mut responses = Vec::with_capacity(insns.len());
struct St {
uh: UpdateHandler,
auth: Authorisation<InstanceName>,
have_deleted: bool,
}
impl St {
#[throws(ME)]
fn flush<'igr, 'ig>(&mut self,
ig: &'igr mut InstanceGuard<'ig>,
how: MgmtGameUpdateMode,
who: &Html) {
mem::replace(
&mut self.uh,
UpdateHandler::from_how(how),
)
.complete(ig, who)?;
}
#[throws(ME)]
fn flushu<'igr, 'ig>(&mut self,
igu: &'igr mut Unauthorised<InstanceGuard<'ig>, InstanceName>,
how: MgmtGameUpdateMode,
who: &Html) {
let ig = igu.by_mut(self.auth);
self.flush(ig, how, who)?;
}
}
let mut uh_auth: Option<St> = None;
let flush_uh = |st: &mut St, igu: &'_ mut _| st.flushu(igu, how, &who);
let mut insns: VecDeque<_> = insns.into();
let res = (||{
while let Some(insn) = insns.pop_front() {
trace_dbg!("exeucting game insns", insn);
if_chain!{
if let MGI::AddPieces{..} = &insn;
if let Some(ref mut st) = &mut uh_auth;
if st.have_deleted == true;
then {
flush_uh(st,igu)?;
}
}
let was_delete = matches!(&insn, MGI::DeletePiece(..));
let (updates, resp, unprepared, expand, ig) =
execute_game_insn(cs, ag, igu, insn, &who,
&mut to_permute)?;
let st = uh_auth.get_or_insert_with(||{
let auth = Authorisation::promise_for(&*ig.name);
let uh = UpdateHandler::from_how(how);
St { uh, auth, have_deleted: false }
});
st.have_deleted |= was_delete;
st.uh.accumulate(ig, updates)?;
if matches!(&resp, MGR::InsnExpanded) {
let mut expand: VecDeque<_> = expand.into();
expand.append(&mut insns);
insns = expand;
} else {
assert!(expand.is_empty())
}
responses.push(resp);
PrepareUpdatesBuffer::only_unprepared_with(unprepared, ||{
st.flush(ig,how,&who)?;
Ok::<_,ME>(ig)
})?;
}
if let Some(ref mut st) = uh_auth {
flush_uh(st,igu)?;
}
Ok(None)
})();
if let Some(mut st) = uh_auth {
flush_uh(&mut st, igu)?;
igu.by_mut(st.auth).save_game_now()?;
}
Ok::<_,ME>(MgmtResponse::AlterGame {
responses,
error: res.unwrap_or_else(Some),
})
})();
(r, {
to_permute.implement_auth(&mut *igu)
})
});
PrepareUpdatesBuffer::only_unprepared_with(uu, ||Ok::<_,Void>(
igu.by_mut(Authorisation::promise_any())
)).void_unwrap();
ok?
}
#[derive(Debug,Default)]
struct UpdateHandlerBulk {
pieces: HashMap<PieceId, PieceUpdateOp<(),()>>,
logs: bool,
raw: Vec<PreparedUpdateEntry>,
}
#[derive(Debug)]
enum UpdateHandler {
Bulk(UpdateHandlerBulk),
Online,
}
impl UpdateHandler {
fn from_how(how: MgmtGameUpdateMode) -> Self {
use UpdateHandler::*;
match how {
MgmtGameUpdateMode::Bulk => Bulk(default()),
MgmtGameUpdateMode::Online => Online,
}
}
#[throws(SVGProcessingError)]
fn accumulate(&mut self, g: &mut Instance,
updates: ExecuteGameChangeUpdates) {
let mut raw = updates.raw.unwrap_or_default();
use UpdateHandler::*;
match self {
Bulk(bulk) => {
for (upiece, uuop) in updates.pcs {
use PieceUpdateOp::*;
let oe = bulk.pieces.get(&upiece);
let ne = match (oe, uuop) {
( None , e ) => Some( e ),
( Some( Insert (()) ) , Delete() ) => None,
( Some( InsertQuiet(()) ) , Delete() ) => None,
( Some( Insert (()) ) , _ ) => Some( Insert (()) ),
( Some( InsertQuiet(()) ) , _ ) => Some( InsertQuiet(()) ),
( Some( Delete ( ) ) , _ ) => Some( Modify (()) ),
( _ , _ ) => Some( Modify (()) ),
};
trace_dbg!("accumulate", upiece, oe, uuop, ne);
match ne {
Some(ne) => { bulk.pieces.insert(upiece, ne); },
None => { bulk.pieces.remove(&upiece); },
};
}
bulk.logs |= updates.log.len() != 0;
bulk.raw.append(&mut raw);
}
Online => {
let estimate = updates.pcs.len() + updates.log.len();
let mut buf = PrepareUpdatesBuffer::new(g, Some(estimate));
for (upiece, uuop) in updates.pcs {
buf.piece_update(upiece, &None, PUOs::Simple(uuop));
}
buf.log_updates(updates.log);
buf.raw_updates(raw);
}
}
}
#[throws(SVGProcessingError)]
fn complete(self, g: &mut InstanceGuard, who: &Html) {
use UpdateHandler::*;
match self {
Bulk(bulk) => {
let mut buf = PrepareUpdatesBuffer::new(g, None);
for (upiece, uuop) in bulk.pieces {
buf.piece_update(upiece, &None, PUOs::Simple(uuop));
}
if bulk.logs {
buf.log_updates(vec![LogEntry {
html: hformat!("{} (re)configured the game", who),
}]);
}
buf.raw_updates(bulk.raw);
}
Online => {}
}
}
}
impl CommandStream<'_> {
#[throws(CSE)]
pub fn mainloop(&mut self) {
loop {
use PacketFrameReadError::*;
match {
self.chan.read.inner_mut().set_timeout(Some(IDLE_TIMEOUT));
let r = self.chan.read.read_withbulk::<MgmtCommand>();
r
} {
Ok((cmd, mut rbulk)) => {
execute_and_respond(&mut self.d, cmd, &mut rbulk,
&mut self.chan.write)?;
},
Err(EOF) => break,
Err(IO(e)) if e.kind() == ErrorKind::TimedOut => {
info!("{}: idle timeout reading command stream", &self.d);
self.write_error(ME::IdleTimeout)?;
break;
}
Err(IO(e)) => Err(e).context("read command stream")?,
Err(Parse(s)) => self.write_error(ME::CommandParseFailed(s))?,
}
}
}
#[throws(CSE)]
pub fn write_error(&mut self, error: MgmtError) {
let resp = MgmtResponse::Error { error };
self.chan.write.write(&resp).context("swrite error to command stream")?;
}
}
impl CommandStreamData<'_> {
#[throws(MgmtError)]
fn current_account(&self) -> &AccountSpecified {
self.account.as_ref().ok_or(ME::SpecifyAccount)?
}
}
impl CommandListener {
#[throws(StartupError)]
pub fn new() -> Self {
let path = &config().command_socket;
match fs::remove_file(path) {
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
r => r,
}
.with_context(|| format!("remove socket {:?} before we bind", &path))?;
let listener = UnixListener::bind(path)
.with_context(|| format!("bind command socket {:?}", &path))?;
fs::set_permissions(path, unix::fs::PermissionsExt::from_mode(0o666))
.with_context(|| format!("chmod sommand socket {:?}", &path))?;
CommandListener { listener }
}
#[throws(StartupError)]
pub fn spawn(mut self) {
thread::spawn(move ||{
loop {
self.accept_one().unwrap_or_else(
|e| error!("accept/spawn failed: {:?}", e)
);
}
})
}
#[throws(CSE)]
fn accept_one(&mut self) {
let (conn, _caller) = self.listener.accept().context("accept")?;
let mut desc = format!("{:>5}", conn.as_raw_fd());
info!("command connection {}: accepted", &desc);
thread::spawn(move||{
let (chan, authstate) = (||{
let euid = conn.initial_peer_credentials()
.map(|creds| creds.euid())
.map_err(|e| ConnectionEuidDiscoverError(format!("{}", e)));
#[derive(Error,Debug)]
struct EuidLookupError(String);
display_as_debug!{EuidLookupError}
impl<E> From<&E> for EuidLookupError where E: Display {
fn from(e: &E) -> Self { EuidLookupError(format!("{}", e)) }
}
let user_desc: String = (||{
let euid = euid.clone()?;
let pwent = Passwd::from_uid(euid);
let show_username =
pwent.map_or_else(|| format!("<euid {}>", euid),
|p| p.name);
<Result<_,AE>>::Ok(show_username)
})().unwrap_or_else(|e| format!("<error: {}>", e));
write!(&mut desc, " user={}", user_desc)?;
let chan = MgmtChannel::new_timed(conn)?;
let authstate = AuthState::None { euid: euid.map(Uid::from_raw) };
Ok::<_,StartupError>((chan, authstate))
})().map_err(|e|{
warn!("command connection {}: setup failed: {:?}", &desc, e);
()
})?;
let d = CommandStreamData {
account: None,
conn_desc: &desc,
authstate,
};
let mut cs = CommandStream { chan, d };
match {
cs.mainloop()
} {
Ok(()) => info!("{}: disconnected", &cs.d),
Err(e) => info!("{}: unrecoverable error: {}", &cs.d, e.d()),
}
Ok::<_,()>(())
});
}
}
#[derive(Debug,Error,Clone)]
#[error("connection euid lookup failed (at connection initiation): {0}")]
pub struct ConnectionEuidDiscoverError(String);
impl From<ConnectionEuidDiscoverError> for AuthorisationError {
fn from(e: ConnectionEuidDiscoverError) -> AuthorisationError {
AuthorisationError(format!("{}", e))
}
}
impl CommandStreamData<'_> {
#[throws(AuthorisationError)]
fn authorised_uid(&self, wanted: Option<Uid>, xinfo: Option<&str>)
-> Authorisation<Uid> {
let &client_euid = match &self.authstate {
AuthState::Superuser { euid, .. } => euid,
AuthState::None { euid, .. } => euid,
AuthState::Ssh { .. } => throw!(anyhow!(
"{}: cannot authorise by uid as now in AuthState::Ssh", &self)),
}.as_ref().map_err(|e| e.clone())?;
let server_uid = Uid::current();
if client_euid.is_root() ||
client_euid == server_uid ||
Some(client_euid) == wanted
{
return Authorisation::promise_for(&client_euid);
}
throw!(anyhow!("{}: euid mismatch: client={:?} server={:?} wanted={:?}{}",
&self, client_euid, server_uid, wanted,
xinfo.unwrap_or("")));
}
fn map_auth_err(&self, ae: AuthorisationError) -> MgmtError {
warn!("{}: authorisation error: {}",
self, ae.0);
MgmtError::AuthorisationError
}
}
impl CommandStreamData<'_> {
pub fn superuser(&self) -> Option<AuthorisationSuperuser> {
match self.authstate {
AuthState::Superuser { auth, .. } => Some(auth),
_ => None
}
}
pub fn is_superuser<T:Serialize>(&self) -> Option<Authorisation<T>> {
self.superuser().map(Into::into)
}
#[throws(MgmtError)]
pub fn check_acl_modify_player<'igr, 'ig: 'igr,
P: Into<PermSet<TablePermission>>>(
&self,
ag: &AccountsGuard,
ig: &'igr mut Unauthorised<InstanceGuard<'ig>, InstanceName>,
player: PlayerId,
p: P,
) -> (&'igr mut InstanceGuard<'ig>,
Authorisation<InstanceName>)
{
let ipl_unauth = {
let ig = ig.by_ref(Authorisation::promise_any());
ig.iplayers.byid(player)?
};
let how = PCH::InstanceOrOnlyAffectedAccount(ipl_unauth.ipl.acctid);
let (ig, auth) = self.check_acl(ag, ig, how, p)?;
(ig, auth)
}
#[throws(MgmtError)]
pub fn check_acl_modify_pieces<'igr, 'ig: 'igr>(
&self,
ag: &AccountsGuard,
ig: &'igr mut Unauthorised<InstanceGuard<'ig>, InstanceName>,
) -> (
&'igr mut InstanceGuard<'ig>,
ModifyingPieces,
Authorisation<InstanceName>,
) {
let p = &[TP::ChangePieces];
let (ig, auth) = self.check_acl(ag, ig, PCH::Instance, p)?;
let modperm = ig.modify_pieces();
(ig, modperm, auth)
}
#[throws(MgmtError)]
pub fn check_acl<'igr, 'ig: 'igr, P: Into<PermSet<TablePermission>>>(
&self,
ag: &AccountsGuard,
ig: &'igr mut Unauthorised<InstanceGuard<'ig>, InstanceName>,
how: PermissionCheckHow,
p: P,
) -> (&'igr mut InstanceGuard<'ig>, Authorisation<InstanceName>) {
#[throws(MgmtError)]
fn get_auth(cs: &CommandStreamData,
ag: &AccountsGuard,
ig: &mut Unauthorised<InstanceGuard, InstanceName>,
how: PermissionCheckHow,
p: PermSet<TablePermission>)
-> Authorisation<InstanceName>
{
if let Some(superuser) = cs.superuser() {
return superuser.into();
}
let current_account = cs.current_account()?;
let (_subject_record, subject_acctid) =
ag.lookup(¤t_account.notional_account)?;
let subject_is = |object_acctid: AccountId|{
if subject_acctid == object_acctid {
let auth: Authorisation<InstanceName>
= Authorisation::promise_any();
return Some(auth);
}
return None;
};
if let Some(auth) = match how {
PCH::InstanceOrOnlyAffectedAccount(object_acctid) => {
subject_is(object_acctid)
},
PCH::InstanceOrOnlyAffectedPlayer(object_player) => {
if_chain!{
if let Some(object_ipr) =
ig.by_ref(Authorisation::promise_any()).iplayers
.get(object_player);
then { subject_is(object_ipr.ipl.acctid) }
else { None }
}
}
PCH::Instance => None,
} {
return auth;
}
let auth = {
let subject = ¤t_account.cooked;
let (acl, owner) = {
let ig = ig.by_ref(Authorisation::promise_any());
(&ig.acl, &ig.name.account)
};
let owner_account = owner.to_string();
let owner_account = Some(owner_account.as_str());
let eacl = EffectiveACL { owner_account, acl };
eacl.check(subject, p)?
};
auth
}
let auth = get_auth(self, ag, ig, how, p.into())?;
(ig.by_mut(auth), auth)
}
#[throws(MgmtError)]
fn accountrecord_from_spec(&self, spec: Option<Box<dyn PlayerAccessSpec>>)
-> Option<AccessRecord> {
spec
.map(|spec| AccessRecord::from_spec(spec, self.superuser()))
.transpose()?
}
}
#[throws(MgmtError)]
fn authorise_for_account(
cs: &CommandStreamData,
_accounts: &AccountsGuard,
wanted: &AccountName,
) -> Authorisation<AccountName> {
if let Some(y) = cs.is_superuser() {
return y;
}
let currently = &cs.current_account()?;
if ¤tly.notional_account != wanted {
throw!(MgmtError::AuthorisationError)
}
currently.auth
}
#[throws(MgmtError)]
fn authorise_by_account(cs: &CommandStreamData, ag: &AccountsGuard,
wanted: &InstanceName)
-> Authorisation<InstanceName> {
let current = cs.current_account()?;
ag.check(¤t.notional_account)?;
if let Some(y) = cs.superuser() {
return y.so_promise();
}
if current.notional_account == wanted.account {
current.auth.map(
|account: &AccountName| Box::leak(Box::new(InstanceName {
account: account.clone(),
game: wanted.game.clone(),
}))
)
} else {
throw!(ME::AuthorisationError);
}
}
#[throws(MgmtError)]
fn authorise_scope_direct(cs: &CommandStreamData, ag: &AccountsGuard,
wanted: &AccountScope)
-> Authorisation<AccountScope> {
do_authorise_scope(cs, ag, wanted)
.map_err(|e| cs.map_auth_err(e))?
}
#[throws(AuthorisationError)]
fn do_authorise_scope(cs: &CommandStreamData, ag: &AccountsGuard,
wanted: &AccountScope)
-> Authorisation<AccountScope> {
match &cs.authstate {
&AuthState::Superuser { auth, .. } => return auth.into(),
&AuthState::Ssh { ref key, auth } => {
let wanted_base_account = AccountName {
scope: wanted.clone(),
subaccount: default(),
};
if_chain!{
if let Ok::<_,AccountNotFound>
((record, _acctid)) = ag.lookup(&wanted_base_account);
if let
Some(auth) = record.ssh_keys.check(ag, key, auth);
then { return Ok(auth) }
else { throw!(AuthorisationError("ssh key not authorised".into())); }
}
},
_ => {},
}
match &wanted {
AccountScope::Server => {
let y: Authorisation<Uid> = {
cs.authorised_uid(None,None)?
};
y.so_promise()
}
AccountScope::Ssh{..} => {
throw!(AuthorisationError(
"account must be accessed via ssh proxy"
.into()));
}
AccountScope::Unix { user: wanted } => {
struct InUserList;
let y: Authorisation<(Passwd,Uid,InUserList)> = {
struct AuthorisedIf { authorised_for: Option<Uid> }
const SERVER_ONLY: (AuthorisedIf, Authorisation<InUserList>) = (
AuthorisedIf { authorised_for: None },
Authorisation::promise_for(&InUserList),
);
let pwent = Passwd::from_name(wanted)
.map_err(
|e| anyhow!("looking up requested username {:?}: {:?}",
&wanted, &e)
)?
.ok_or_else(
|| AuthorisationError(format!(
"requested username {:?} not found", &wanted
))
)?;
let pwent_ok = Authorisation::promise_for(&pwent);
let ((uid, in_userlist_ok), xinfo) = (||{ <Result<_,AE>>::Ok({
let allowed = BufReader::new(match File::open(USERLIST) {
Err(e) if e.kind() == ErrorKind::NotFound => {
return Ok((
SERVER_ONLY,
Some(format!(" user list {} does not exist", USERLIST))
))
},
r => r,
}?);
allowed
.lines()
.filter_map(|le| match le {
Ok(l) if l.trim() == wanted => Some(
Ok((
(AuthorisedIf{ authorised_for: Some(
Uid::from_raw(pwent.uid)
)},
Authorisation::promise_for(&InUserList),
),
None
))
),
Ok(_) => None,
Err(e) => Some(<Result<_,AE>>::Err(e.into())),
})
.next()
.unwrap_or_else(
|| Ok((
SERVER_ONLY,
Some(format!(" requested username {:?} not in {}",
&wanted, USERLIST)),
))
)?
})})()?;
let info = xinfo.as_deref();
let uid_ok = cs.authorised_uid(uid.authorised_for, info)?;
(pwent_ok, uid_ok, in_userlist_ok).combine()
};
y.so_promise()
},
}
}