// Copyright 2020-2021 Ian Jackson and contributors to Otter // SPDX-License-Identifier: AGPL-3.0-or-later // There is NO WARRANTY. // management API implementation use otter::crates::*; use otter_support::imports::*; use super::*; use otter::commands::*; use authproofs::*; // ---------- newtypes, type aliases, basic definitions ---------- 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; // don't make not fit in i32 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, d: CommandStreamData<'d>, } struct CommandStreamData<'d> { conn_desc: &'d str, account: Option, 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; #[derive(Debug)] enum AuthState { None { euid: Euid }, Superuser { euid: Euid, auth: AuthorisationSuperuser }, Ssh { key: sshkeys::KeySpec, auth: Authorisation }, } #[derive(Debug,Clone)] struct AccountSpecified { notional_account: AccountName, // might not exist cooked: String, // account.to_string() auth: Authorisation, // but we did check permissions } #[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, ]; // ========== management API ========== // ---------- management command implementations //#[throws(CSE)] fn execute_and_respond(cs: &mut CommandStreamData, cmd: MgmtCommand, bulk_upload: &mut ReadFrame, for_response: &mut FrameWriter) -> Result<(), CSE> where W: Write { let mut bulk_download: Option> = 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) { ( AccountsGuard::lock(), Instance::lookup_by_name_unauth(game)?, ) } #[throws(MgmtError)] fn access_bundles<'ig,F,R>( cs: &mut CommandStreamData, ag: &AccountsGuard, gref: &'ig Unauthorised, perms: &[TablePermission], f: &mut F, ) -> (R, Authorisation) where F: FnMut( &mut InstanceGuard<'ig>, BundlesGuard<'ig>, ) -> Result { 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) { 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(spec: Option, 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)); // If the timeout fires after the bulk data has all arrived, it // won't take effect, because: it only takes effect when we try // to read from the stresm, and after we have the data, we // won't read again until we go on to the next command - which // will have its own 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::,_>>() ?; 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::>() ) )?; 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 = 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(()) } // ---------- game command implementations ---------- type ExecuteGameInsnResults<'igr, 'ig> = ( ExecuteGameChangeUpdates, MgmtGameResponse, UnpreparedUpdates, // These happena after everything else Vec, &'igr mut InstanceGuard<'ig>, ); //#[throws(ME)] fn execute_game_insn<'cs, 'igr, 'ig: 'igr>( cs: &'cs CommandStreamData, ag: &'_ mut AccountsGuard, ig: &'igr mut Unauthorised, InstanceName>, update: MgmtGameInstruction, who: &Html, to_permute: &mut ToRecalculate, ) -> Result ,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, P: Into>> ( cs: &'cs CommandStreamData, ag: &AccountsGuard, ig: &'igr mut Unauthorised, 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, 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); // does updatenocc ig.save_game_now()?; // we handled the update ourselves, return no update info, just MGR (gen, MGR::Synch(gen)) } #[throws(MgmtError)] fn update_links<'igr, 'ig: 'igr, 'cs, F: FnOnce(&mut Arc) -> Result> ( cs: &'cs CommandStreamData, ag: &'_ mut AccountsGuard, ig: &'igr mut Unauthorised, 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, InstanceName>, f: Box) -> Result + '_> ) -> ExecuteGameInsnResults<'igr, 'ig> { let ig = cs.check_acl(ag, ig, PCH::Instance, &[TP::ChangePieces])?.0; // Clear out old stuff 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, InstanceName>, who: &'_ Html, get_spec_toml: Box Result>, ) -> 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()))?; // Define new stuff: 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, InstanceName>, player: PlayerId, perm: TablePermission, ) -> (&'igr mut InstanceGuard<'ig>, Authorisation) { let (ig, auth) = self.check_acl(ag, ig, PCH::InstanceOrOnlyAffectedPlayer(player), &[perm])?; fn auth_map(n: &InstanceName) -> &AccountName { &n.account } // let 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 { // todo: something more sophisticated would be nice let pri = PieceRenderInstructions::new_visible( // visible id is internal one here 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::,_>>()?; 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::,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(); // todo want a FromIterator impl 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(|| "".into()) , (||{ let (record, _) = ag.lookup(ipl?.acctid).ok()?; Some(record.account.to_string()) })() .unwrap_or_else(|| "".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 { "".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> = 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(()))); })(); // <- no ?, infallible (to avoid leaking ilk) 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() { // todo: do this only if there are no hidden pieces? 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) -> Vec { 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 { /* ok */ } 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 { "partial data?!".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) } // ---------- how to execute game commands & handle their updates ---------- #[throws(ME)] fn execute_for_game<'cs, 'igr, 'ig: 'igr>( cs: &'cs CommandStreamData, ag: &mut AccountsGuard, igu: &'igr mut Unauthorised, InstanceName>, insns: Vec, 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, 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, InstanceName>, how: MgmtGameUpdateMode, who: &Html) { let ig = igu.by_mut(self.auth); self.flush(ig, how, who)?; } } let mut uh_auth: Option = 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 { // This makes sure that all the updates we have queued up // talking about the old PieceId, will be Prepared (ie, the // vpid lookup done) before we reuse the slot and render the // vpid lookup impossible. 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>, logs: bool, raw: Vec, } #[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) { // We preserve Quietness rather than coalescing. This avoids // many more cases here, and this code is used for mgmt // updates, which are generally Quiet anyway. ( 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 => {} } } } // ========== general implementation ========== // ---------- core listener implementation ---------- 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::(); 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 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), |p| p.name); >::Ok(show_username) })().unwrap_or_else(|e| format!("", 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::<_,()>(()) }); } } //---------- authorisation ---------- #[derive(Debug,Error,Clone)] #[error("connection euid lookup failed (at connection initiation): {0}")] pub struct ConnectionEuidDiscoverError(String); impl From for AuthorisationError { fn from(e: ConnectionEuidDiscoverError) -> AuthorisationError { AuthorisationError(format!("{}", e)) } } impl CommandStreamData<'_> { #[throws(AuthorisationError)] fn authorised_uid(&self, wanted: Option, xinfo: Option<&str>) -> Authorisation { 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 { match self.authstate { AuthState::Superuser { auth, .. } => Some(auth), _ => None } } pub fn is_superuser(&self) -> Option> { self.superuser().map(Into::into) } #[throws(MgmtError)] pub fn check_acl_modify_player<'igr, 'ig: 'igr, P: Into>>( &self, ag: &AccountsGuard, ig: &'igr mut Unauthorised, InstanceName>, player: PlayerId, p: P, ) -> (&'igr mut InstanceGuard<'ig>, Authorisation) { 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, InstanceName>, ) -> ( &'igr mut InstanceGuard<'ig>, ModifyingPieces, Authorisation, ) { 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>>( &self, ag: &AccountsGuard, ig: &'igr mut Unauthorised, InstanceName>, how: PermissionCheckHow, p: P, ) -> (&'igr mut InstanceGuard<'ig>, Authorisation) { #[throws(MgmtError)] fn get_auth(cs: &CommandStreamData, ag: &AccountsGuard, ig: &mut Unauthorised, how: PermissionCheckHow, p: PermSet) -> Authorisation { 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 = 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>) -> Option { spec .map(|spec| AccessRecord::from_spec(spec, self.superuser())) .transpose()? } } #[throws(MgmtError)] fn authorise_for_account( cs: &CommandStreamData, _accounts: &AccountsGuard, wanted: &AccountName, ) -> Authorisation { 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 { 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( // Not executed, exists as a proof. // we need this Box::leak because map wants us to return a ref // borrowing from the incoming subject, which would imply narrowing // of scope and of course we are widening scope here. We're // saying that the account can access all its games. |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 { // Usually, use authorise_by_account 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 { 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 = { cs.authorised_uid(None,None)? }; y.so_promise() } AccountScope::Ssh{..} => { // Should have been dealt with earlier, when we checked authstate. 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 } const SERVER_ONLY: (AuthorisedIf, Authorisation) = ( 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) = (||{ >::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(>::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() }, } }