From: Ian Jackson Date: Wed, 21 Oct 2020 23:54:12 +0000 (+0100) Subject: wip new account etc. X-Git-Tag: otter-0.2.0~611 X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~ianmdlvl/git?a=commitdiff_plain;h=ab95585eb41c55dd8ac98985a9ee856b35fb7ff8;p=otter.git wip new account etc. Signed-off-by: Ian Jackson --- diff --git a/src/accounts.rs b/src/accounts.rs index 88457698..83b69035 100644 --- a/src/accounts.rs +++ b/src/accounts.rs @@ -184,8 +184,13 @@ impl AccountRecord { pub mod loaded_acl { use crate::imports::*; - pub trait Perm : FromPrimitive + ToPrimitive - + Copy + Eq + Hash + Sync + Send + 'static { } + pub trait Perm : FromPrimitive + ToPrimitive + + Copy + Eq + Hash + Sync + Send + 'static + { + type Auth; + const NOT_FOUND : MgmtError; + const TEST_EXISTENCE : Self; + } #[derive(Copy,Clone,Debug)] pub struct PermSet (u64, PhantomData<&'static P>); @@ -197,8 +202,15 @@ pub mod loaded_acl { } #[derive(Debug,Clone)] + #[derive(Serialize,Deserialize)] + #[serde(from="Acl")] + #[serde(into="Acl")] pub struct LoadedAcl (Vec>); + impl Default for LoadedAcl

{ + fn default() -> Self { LoadedAcl(default()) } + } + #[derive(Debug,Clone)] struct LoadedAclEntry { pat: glob::Pattern, @@ -228,19 +240,25 @@ pub mod loaded_acl { } #[throws(MgmtError)] - fn check(&self, account_name: &str, p: PermSet

) { + fn check(&self, subject: &str, p: PermSet

) -> Authorisation { let mut needed = p.0; assert!(needed != 0); + let test_existence = P::test_existence().to_primitive(); + needed |= test_existence; for AclEntryRef { pat, allow, deny } in self.entries() { if !match pat { - Left(owner) => owner == account_name, - Right(pat) => pat.matches(account_name), + Left(owner) => owner == subject, + Right(pat) => pat.matches(subject), } { continue } if needed & deny != 0 { break } - needed &= !allow; - if needed == 0 { return Ok(()) } + if allow != 0 { needed &= !(allow | test_existence) } + if needed == 0 { return Ok(Authorisation::authorise_any()) } } - Err(MgmtError::PermissionDenied) + Err(if needed & test_existence != 0 { + P::NOT_FOUND + } else { + MgmtError::PermissionDenied + }) } } diff --git a/src/cmdlistener.rs b/src/cmdlistener.rs index 7f43c844..07b2f062 100644 --- a/src/cmdlistener.rs +++ b/src/cmdlistener.rs @@ -26,6 +26,7 @@ type ME = MgmtError; from_instance_lock_error!{MgmtError} type AS = AccountScope; +type TP = TablePermission; const USERLIST : &str = "/etc/userlist"; const CREATE_PIECES_MAX : u32 = 300; @@ -55,7 +56,7 @@ struct CommandStream<'d> { struct AccountSpecified { account: AccountName, cooked: String, // account.to_string() - auth: Authorisation, + auth: Authorisation, } // ========== management API ========== @@ -70,16 +71,17 @@ fn execute(cs: &mut CommandStream, cmd: MgmtCommand) -> MgmtResponse { Noop => Fine, SetAccount(wanted_account) => { - let authorised = authorise_scope_direct(cs, &wanted_account.scope)?; - cs.account = Some(( - wanted_account, - authorised.therefore_ok(), - )); + let auth = authorise_scope_direct(cs, &wanted_account.scope)?; + cs.account = Some(AccountSpecified { + account: wanted_account, + cooked: wanted_account.to_string(), + auth: auth.therefore_ok(), + }); Fine }, CreateGame { game, insns } => { - let authorised = authorise_by_account(cs, &game.scope)?; + let authorised = authorise_by_account(cs, &game)?; let gs = crate::gamestate::GameState { table_size : DEFAULT_TABLE_SIZE, @@ -90,7 +92,8 @@ fn execute(cs: &mut CommandStream, cmd: MgmtCommand) -> MgmtResponse { max_z: default(), }; - let gref = Instance::new(game, gs, authorised)?; + let acl = default(); + let gref = Instance::new(game, gs, acl, authorised)?; let ig = gref.lock()?; execute_for_game(cs, &mut Unauthorised::of(ig), @@ -111,11 +114,11 @@ fn execute(cs: &mut CommandStream, cmd: MgmtCommand) -> MgmtResponse { ListGames { all } => { let (scope, auth) = if all == Some(true) { let auth = authorise_scope_direct(cs, &AS::Server)?; - (None, auth) + (None, auth.therefore_ok()) } else { - let (account, auth) = cs.account.as_ref() + let AccountSpecified { account, auth, .. } = cs.account.as_ref() .ok_or(ME::SpecifyAccount)?; - (Some(&account.scope), *auth) + (Some(account), *auth) }; let mut games = Instance::list_names(scope, auth); games.sort_unstable(); @@ -143,11 +146,11 @@ type ExecuteGameInsnResults = ( MgmtGameResponse, ); -#[throws(ME)] +//#[throws(ME)] fn execute_game_insn(cs: &CommandStream, ig: &mut Unauthorised, update: MgmtGameInstruction) - -> ExecuteGameInsnResults { + -> Result { type U = ExecuteGameChangeUpdates; use MgmtGameInstruction::*; use MgmtGameResponse::*; @@ -155,21 +158,26 @@ fn execute_game_insn(cs: &CommandStream, type Resp = MgmtGameResponse; let who = &cs.who; - fn readonly ExecuteGameInsnResults>( - cs: &CommandStream, - ig: &Unauthorised, - p: PermSet, - f: F) -> ExecuteGameInsnResults + fn readonly MgmtGameResponse, + P: Into>> + + ( + cs: &CommandStream, + ig: &Unauthorised, + p: P, + f: F + ) -> ExecuteGameInsnResults { - let ig = ig.check_acl(&cs.account.as_ref()?.cooked)?; + let ig = cs.check_acl(p)?; let resp = f(ig); (U{ pcs: vec![], log: vec![], raw: None }, resp) } - match update { - Noop { } => readonly(ig, Fine), + let y = match update { + Noop { } => readonly(cs,ig, &[], |ig| Fine), Insn::SetTableSize(size) => { + let ig = cs.check_acl(ig, &[TP::ChangePieces])?; ig.gs.table_size = size; (U{ pcs: vec![], log: vec![ LogEntry { @@ -180,42 +188,64 @@ fn execute_game_insn(cs: &CommandStream, Fine) } - Insn::AddPlayer(pl) => { - if ig.gs.players.values().any(|p| p.nick == pl.st.nick) { - Err(ME::AlreadyExists)?; - } + Insn::AddPlayer(add) => { + // todo some kind of permissions check for player too + let ig = cs.check_acl(ig, &[TP::AddPlayer])?; + let nick = add.nick.ok_or(ME::ParameterMissing)?; let logentry = LogEntry { html: Html(format!("{} added a player: {}", &who, - htmlescape::encode_minimal(&pl.st.nick))), + htmlescape::encode_minimal(&nick))), }; - let timezone = pl.timezone.as_ref().map(String::as_str) + let timezone = add.timezone.as_ref().map(String::as_str) .unwrap_or(""); let tz = match Timezone::from_str(timezone) { Ok(tz) => tz, Err(x) => match x { }, }; - let (player, logentry) = ig.player_new(pl.st, tz, logentry)?; + let st = PlayerState { + tz, + account: add.account, + nick: nick.to_string(), + }; + let (player, logentry) = ig.player_new(st, tz, logentry)?; (U{ pcs: vec![], log: vec![ logentry ], raw: None }, Resp::AddPlayer(player)) }, - Insn::ListPieces => readonly(ig, { + Insn::ListPieces => readonly(cs,ig, &[TP::ViewPublic], |ig|{ // xxx put something in log let pieces = ig.gs.pieces.iter().map(|(piece,p)|{ let &PieceState { pos, face, .. } = p; - let pinfo = ig.pieces.get(piece)?; + let pinfo = ig.ipieces.get(piece)?; let desc_html = pinfo.describe_html(None); let itemname = pinfo.itemname().to_string(); let bbox = pinfo.bbox_approx(); - Some(MgmtGamePieceInfo { piece, pos, face, desc_html, bbox, itemname }) + let lens = TransparentLens { }; + Some(MgmtGamePieceInfo { + piece, itemname, + visible: + if let TransparentLens { } = lens { + Some(MgmtGamePieceVisibleInfo { + pos, face, desc_html, bbox + }) + } else { + None + } + }) }).flatten().collect(); Resp::Pieces(pieces) }), - RemovePlayer(player) => { - let old_state = ig.player_remove(player)?; + RemovePlayer(account) => { + // todo let you remove yourself unconditionally + let ig = cs.check_acl(ig, &[TP::RemovePlayer])?; + let player = ig.gs.players.iter() + .filter_map(|(k,v)| if v == &account { Some(k) } else { None }) + .next() + .ok_or(ME::PlayerNotFound)?; + let (_, old_state) = ig.player_remove(player)?; (U{ pcs: vec![], log: old_state.iter().map(|pl| LogEntry { html: Html(format!("{} removed a player: {}", &who, @@ -225,15 +255,29 @@ fn execute_game_insn(cs: &CommandStream, Fine) }, - Insn::Info => readonly(ig, { - let players = ig.gs.players.clone(); + Insn::Info => readonly(cs,ig, &[TP::ViewPublic], |ig|{ + let players = ig.gs.players.iter().map( + |(player, &account)| { + let account = account.clone(); + let info = match ig.iplayers.get(player) { + Some(pl) => MgmtPlayerInfo { + account, + nick: pl.pst.nick.clone(), + }, + None => MgmtPlayerInfo { + account, + nick: format!(""), + }, + }; + (player, info) + }).collect(); let table_size = ig.gs.table_size; let info = MgmtGameResponseGameInfo { table_size, players }; Resp::Info(info) }), ResetPlayerAccess(player) => { - let token = ig.players_access_reset(player)?; + let token = ig.player_access_reset(player)?; (U{ pcs: vec![], log: vec![], raw: None }, @@ -241,7 +285,7 @@ fn execute_game_insn(cs: &CommandStream, } RedeliverPlayerAccess(player) => { - let token = ig.players_access_redeliver(player)?; + let token = ig.player_access_redeliver(player)?; (U{ pcs: vec![], log: vec![], raw: None }, @@ -324,7 +368,8 @@ fn execute_game_insn(cs: &CommandStream, raw: None }, Fine) }, - } + }; + Ok(y) } // ---------- how to execute game commands & handle their updates ---------- @@ -572,27 +617,31 @@ impl CommandStream<'_> { #[throws(MgmtError)] - pub fn check_acl(&mut self, - ig: &mut Unauthorised, - p: PermSet) -> &mut InstanceGuard { + pub fn check_acl>>( + &mut self, + ig: &mut Unauthorised, + p: P, + ) -> &mut InstanceGuard { + let p = p.into(); let auth = { + let subject = &self.account.as_ref()?.cooked; let acl = self.by(Authorisation::authorise_any()).acl; let eacl = EffectiveAcl { owner_account : &self.account?.to_string(), acl : &acl, }; - eacl.check(p)?; + eacl.check(subject, p)? }; self.by_mut(auth); } } #[throws(MgmtError)] -fn authorise_by_account(cs: &CommandStream, wanted: &AccountScope) - -> Authorisation { +fn authorise_by_account(cs: &CommandStream, wanted: &InstanceName) + -> Authorisation { let currently = &cs.account.as_ref()?.account; - if currently == wanted { - return Authorisation::authorised(currently); + if currently == wanted.account { + return Authorisation::authorised(wanted); } throw!(MgmtError::AuthorisationError) } @@ -712,6 +761,7 @@ mod authproofs { #[error("internal AuthorisationError {0}")] pub struct AuthorisationError(pub String); + #[derive(Debug,Copy,Clone)] pub struct Authorisation (PhantomData); impl Authorisation { diff --git a/src/commands.rs b/src/commands.rs index c24546f5..6aee2344 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -108,7 +108,7 @@ pub struct AccessTokenReport { #[derive(Debug,Clone,Serialize,Deserialize)] pub struct MgmtGameResponseGameInfo { pub table_size: Pos, - pub players: DenseSlotMap, + pub players: SecondarySlotMap, } #[derive(Debug,Clone,Serialize,Deserialize)] @@ -121,6 +121,12 @@ pub struct MgmtPlayerInfo { pub struct MgmtGamePieceInfo { pub piece: PieceId, pub itemname: String, + #[serde(flatten)] + pub visible: Option, +} + +#[derive(Debug,Clone,Serialize,Deserialize)] +pub struct MgmtGamePieceVisibleInfo { pub pos: Pos, pub face: FaceId, pub desc_html: Html, @@ -137,6 +143,7 @@ pub enum MgmtGameUpdateMode { pub enum MgmtError { ParseFailed(String), AuthorisationError, + ParameterMissing, SpecifyAccount, AlreadyExists, NickCollision, diff --git a/src/gamestate.rs b/src/gamestate.rs index 9c3c6682..916af8d0 100644 --- a/src/gamestate.rs +++ b/src/gamestate.rs @@ -61,10 +61,6 @@ pub struct PieceState { pub gen_before_lastclient : Generation, } -#[derive(Debug,Clone,Serialize,Deserialize)] -pub struct PlayerState { -} - #[derive(Debug,Serialize,Deserialize)] pub struct LogEntry { pub html : Html, diff --git a/src/global.rs b/src/global.rs index effc7803..03823a90 100644 --- a/src/global.rs +++ b/src/global.rs @@ -27,8 +27,8 @@ pub struct RawTokenVal(str); #[derive(Clone,Debug,Hash,Eq,PartialEq,Ord,PartialOrd)] pub struct InstanceName { - account: AccountName, - game: String, + pub account: AccountName, + pub game: String, } #[derive(Debug,Clone)] @@ -201,6 +201,7 @@ struct InstanceSaveAccesses { ipieces: PiecesLoadedRef, tokens_players: Vec<(RawTokenStr, PlayerId)>, aplayers: SecondarySlotMap, + acl: Acl, } display_as_debug!{InstanceLockError} @@ -316,23 +317,21 @@ impl Instance { #[throws(MgmtError)] pub fn lookup_by_name_unauth(name: &InstanceName) - -> (Unauthorised, - &InstanceName) + -> Unauthorised { - (Unauthorised::of( + Unauthorised::of( GLOBAL.games.read().unwrap() .get(name) .ok_or(MgmtError::GameNotFound)? .clone() .into() - ), - name) + ) } #[throws(MgmtError)] pub fn lookup_by_name(name: &InstanceName, auth: Authorisation) -> InstanceRef { - Self::lookup_by_name_unauth(name)?.0.by(auth) + Self::lookup_by_name_unauth(name)?.by(auth) } #[throws(InternalError)] @@ -786,7 +785,10 @@ impl InstanceGuard<'_> { |(player, PlayerRecord { pst, .. })| (player, pst.clone()) ).collect(); - let isa = InstanceSaveAccesses { ipieces, tokens_players, aplayers }; + let acl = s.c.g.acl.into(); + let isa = InstanceSaveAccesses { + ipieces, tokens_players, aplayers, acl + }; rmp_serde::encode::write_named(w, &isa) })?; self.c.access_dirty = false; @@ -805,7 +807,7 @@ impl InstanceGuard<'_> { } #[throws(StartupError)] - fn load_game(name: InstanceName) -> InstanceRef { + fn load_game(name: InstanceName) -> Option { { let mut st = GLOBAL.save_area_lock.lock().unwrap(); let st = &mut *st; @@ -818,19 +820,22 @@ impl InstanceGuard<'_> { })().context(lockfile).context("lock global save area")?); } } + let InstanceSaveAccesses:: - { mut tokens_players, mut ipieces, mut aplayers } - = Self::load_something(&name, "a-") - .or_else(|e| { - if let InternalError::Anyhow(ae) = &e { - if let Some(ioe) = ae.downcast_ref::() { - if ioe.kind() == io::ErrorKind::NotFound { - return Ok(Default::default()) - } - } - } - Err(e) - })?; + { mut tokens_players, mut ipieces, mut aplayers, acl } + = match Self::load_something(&name, "a-") { + Ok(data) => data, + Err(e) => if (||{ + let ae = match &e { InternalError::Anyhow(ae) => Some(ae), _=>None }?; + let ioe = ae.downcast_ref::()?; + let is_enoent = ioe.kind() == io::ErrorKind::NotFound; + is_enoent.as_option() + })().is_some() { + return None; + } else { + throw!(e); + }, + }; let mut gs : GameState = Self::load_something(&name, "g-")?; @@ -869,6 +874,7 @@ impl InstanceGuard<'_> { let g = Instance { gs, iplayers, + acl: acl.into(), ipieces: PiecesLoaded(ipieces), name: name.clone(), clients : Default::default(), @@ -898,7 +904,7 @@ impl InstanceGuard<'_> { drop(g); GLOBAL.games.write().unwrap().insert(name.clone(), gref.clone()); info!("loadewd {:?}", &name); - gref + Some(gref) } } diff --git a/src/spec.rs b/src/spec.rs index dd975760..bbb18ad2 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -77,6 +77,8 @@ pub struct AclEntry { #[derive(Hash,Eq,PartialEq,Ord,PartialOrd)] #[derive(FromPrimitive,ToPrimitive)] pub enum TablePermission { + TestExistence, + ViewPublic, AddPlayer, ChangePieces, RemovePlayer, @@ -225,7 +227,11 @@ pub mod implementation { use crate::imports::*; type Insn = crate::commands::MgmtGameInstruction; - impl loaded_acl::Perm for TablePermission { } + impl loaded_acl::Perm for TablePermission { + type Auth = InstanceName; + const TEST_EXISTENCE : Self = TablePermission::TestExistence; + const NOT_FOUND : MgmtError = MgmtError::GameNotFound; + } type TDE = TokenDeliveryError;