chiark / gitweb /
wip new account etc.
authorIan Jackson <ijackson@chiark.greenend.org.uk>
Wed, 21 Oct 2020 23:54:12 +0000 (00:54 +0100)
committerIan Jackson <ijackson@chiark.greenend.org.uk>
Wed, 21 Oct 2020 23:54:12 +0000 (00:54 +0100)
Signed-off-by: Ian Jackson <ijackson@chiark.greenend.org.uk>
src/accounts.rs
src/cmdlistener.rs
src/commands.rs
src/gamestate.rs
src/global.rs
src/spec.rs

index 8845769835b9bc48fd814cfd8cf587700f0617a3..83b69035d5d8bd96ecf22e09f629169749bfb0c5 100644 (file)
@@ -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<P: Perm> (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<P: Perm> (Vec<LoadedAclEntry<P>>);
 
+  impl<P:Perm> Default for LoadedAcl<P> {
+    fn default() -> Self { LoadedAcl(default()) }
+  }
+
   #[derive(Debug,Clone)]
   struct LoadedAclEntry<P: Perm> {
     pat: glob::Pattern,
@@ -228,19 +240,25 @@ pub mod loaded_acl {
     }
 
     #[throws(MgmtError)]
-    fn check(&self, account_name: &str, p: PermSet<P>) {
+    fn check(&self, subject: &str, p: PermSet<P>) -> Authorisation<P::Auth> {
       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
+      })
     }
   }
 
index 7f43c844263c9a4a395d6972f67dd54971d703fe..07b2f06232f3f461ecd08898861211c95cf62be2 100644 (file)
@@ -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<AccountScope>,
+  auth: Authorisation<AccountName>,
 }
 
 // ========== 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<InstanceGuard, InstanceName>,
                      update: MgmtGameInstruction)
-                     -> ExecuteGameInsnResults {
+                     -> Result<ExecuteGameInsnResults,ME> {
   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<F: FnOnce(&mut InstanceGuard) -> ExecuteGameInsnResults>(
-    cs: &CommandStream,
-    ig: &Unauthorised<InstanceGuard, InstanceName>,
-    p: PermSet<TablePermission>,
-    f: F) -> ExecuteGameInsnResults
+  fn readonly<F: FnOnce(&InstanceGuard) -> MgmtGameResponse,
+              P: Into<PermSet<TablePermission>>>
+  
+    (
+      cs: &CommandStream,
+      ig: &Unauthorised<InstanceGuard, InstanceName>,
+      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!("<missing from iplayers table!>"),
+            },
+          };
+          (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<InstanceRef, InstanceName>,
-                   p: PermSet<TablePermission>) -> &mut InstanceGuard {
+  pub fn check_acl<P: Into<PermSet<TablePermission>>>(
+    &mut self,
+    ig: &mut Unauthorised<InstanceGuard, InstanceName>,
+    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<AccountScope> {
+fn authorise_by_account(cs: &CommandStream, wanted: &InstanceName)
+                        -> Authorisation<InstanceName> {
   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<A> (PhantomData<A>);
 
   impl<T> Authorisation<T> {
index c24546f5ba253bd6d972ab94bbc855cf72c367c4..6aee23442f3dec817f2820eb72641e07b74c43d9 100644 (file)
@@ -108,7 +108,7 @@ pub struct AccessTokenReport {
 #[derive(Debug,Clone,Serialize,Deserialize)]
 pub struct MgmtGameResponseGameInfo {
   pub table_size: Pos,
-  pub players: DenseSlotMap<PlayerId, MgmtPlayerInfo>,
+  pub players: SecondarySlotMap<PlayerId, MgmtPlayerInfo>,
 }
 
 #[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<MgmtGamePieceVisibleInfo>,
+}
+
+#[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,
index 9c3c6682e9bcd35e8ebf24eac8356c98e84c0655..916af8d0a38a29bf4b83722835e5df5b84277f9e 100644 (file)
@@ -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,
index effc7803d9ea87f71b26be8502c030a56e576ede..03823a90b3d047b74f9ee4b564418176364d434d 100644 (file)
@@ -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<RawTokenStr, PiecesLoadedRef> {
   ipieces: PiecesLoadedRef,
   tokens_players: Vec<(RawTokenStr, PlayerId)>,
   aplayers: SecondarySlotMap<PlayerId, PlayerState>,
+  acl: Acl<TablePermission>,
 }
 
 display_as_debug!{InstanceLockError}
@@ -316,23 +317,21 @@ impl Instance {
 
   #[throws(MgmtError)]
   pub fn lookup_by_name_unauth(name: &InstanceName)
-                               -> (Unauthorised<InstanceRef, InstanceName>,
-                                   &InstanceName)
+      -> Unauthorised<InstanceRef, InstanceName>
   {
-    (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<InstanceName>)
                         -> 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<InstanceRef> {
     {
       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::<String,ActualPiecesLoaded>
-    { 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::<io::Error>() {
-            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::<io::Error>()?;
+        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)
   }
 }
 
index dd975760ac44375c7a4f8cbfc9b8c161a35ee832..bbb18ad2b0d3ed8a6ab669d8fe01c5653c35d155 100644 (file)
@@ -77,6 +77,8 @@ pub struct AclEntry<Perm: Eq + Hash> {
 #[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;