From 1315084185cdb262e60c3856394e39123e05ae98 Mon Sep 17 00:00:00 2001
From: Juliusz Chroboczek <jch@irif.fr>
Date: Wed, 8 May 2024 16:00:18 +0200
Subject: [PATCH] Implement message permission and shutup command.

---
 CHANGES                   |  2 ++
 README                    |  2 +-
 group/description.go      |  7 ++++---
 group/description_test.go |  8 ++++++--
 group/group_test.go       | 24 ++++++++++++------------
 rtpconn/webclient.go      | 10 +++++++++-
 static/galene.js          | 23 ++++++++++++++++++++---
 token/stateful.go         | 15 +++++++++++++++
 token/stateful_test.go    | 32 ++++++++++++++++----------------
 9 files changed, 85 insertions(+), 38 deletions(-)

diff --git a/CHANGES b/CHANGES
index 9780705..edc0d00 100644
--- a/CHANGES
+++ b/CHANGES
@@ -17,6 +17,8 @@ Galene 0.9 (unreleased)
   * Implemented a contextual menu that triggers on a double click on
     a chat entry.
   * Added a new command "/stopshare".
+  * Added a new permission "message" and new commands "shutup" and
+    "unshutup".
 
 14 April 2024: Galene 0.8.2
 
diff --git a/README b/README
index b985342..59836f4 100644
--- a/README
+++ b/README
@@ -162,7 +162,7 @@ nobody will be able to join the group.  The following fields are allowed:
 
  - `users`: is a dictionary that maps user names to dictionaries with
    entries `password` and `permissions`; `permissions` should be one of
-   `op`, `present` or `passive`;
+   `op`, `present`, `message` or `observe`.
  - `wildcard-user` is a dictionaries with entries `password` and `permissions`
    that will be used for usernames with no matching entry in the `users`
    dictionary;
diff --git a/group/description.go b/group/description.go
index 0ecb20e..91b0a97 100644
--- a/group/description.go
+++ b/group/description.go
@@ -24,8 +24,9 @@ type Permissions struct {
 }
 
 var permissionsMap = map[string][]string{
-	"op":      []string{"op", "present", "token"},
-	"present": []string{"present"},
+	"op":      []string{"op", "present", "message", "token"},
+	"present": []string{"present", "message"},
+	"message": []string{"message"},
 	"observe": []string{},
 	"admin":   []string{"admin"},
 }
@@ -528,7 +529,7 @@ func upgradeDescription(desc *Description) error {
 		desc.Presenter = nil
 	}
 	if desc.Other != nil {
-		upgradeUsers(desc.Other, "observe")
+		upgradeUsers(desc.Other, "message")
 		desc.Other = nil
 	}
 
diff --git a/group/description_test.go b/group/description_test.go
index 4f7e0f6..00bd09c 100644
--- a/group/description_test.go
+++ b/group/description_test.go
@@ -63,11 +63,11 @@ var descJSON = `
     "users": {
         "jch": {"password": "topsecret", "permissions": "op"},
         "john": {"password": "secret", "permissions": "present"},
-        "james": {"password": "secret2", "permissions": "observe"},
+        "james": {"password": "secret2", "permissions": "message"},
         "peter": {"password": "secret4"}
     },
     "wildcard-user":
-        {"permissions": "observe", "password": {"type":"wildcard"}}
+        {"permissions": "message", "password": {"type":"wildcard"}}
 }`
 
 func TestDescriptionJSON(t *testing.T) {
@@ -139,6 +139,10 @@ func TestUpgradeDescription(t *testing.T) {
 	}
 
 	for k, v1 := range d1.Users {
+		if k == "peter" {
+			// not representable in the old format
+			continue
+		}
 		v2 := d2.Users[k]
 		if !reflect.DeepEqual(v1.Password, v2.Password) ||
 			!permissionsEqual(
diff --git a/group/group_test.go b/group/group_test.go
index 5c5afc6..977fa19 100644
--- a/group/group_test.go
+++ b/group/group_test.go
@@ -114,19 +114,19 @@ type credPerm struct {
 var goodClients = []credPerm{
 	{
 		ClientCredentials{Username: &jch, Password: "topsecret"},
-		[]string{"op", "present", "token"},
+		[]string{"op", "present", "message", "token"},
 	},
 	{
 		ClientCredentials{Username: &john, Password: "secret"},
-		[]string{"present"},
+		[]string{"present", "message"},
 	},
 	{
 		ClientCredentials{Username: &james, Password: "secret2"},
-		[]string{},
+		[]string{"message"},
 	},
 	{
 		ClientCredentials{Username: &paul, Password: "secret3"},
-		[]string{},
+		[]string{"message"},
 	},
 	{
 		ClientCredentials{Username: &peter, Password: "secret4"},
@@ -189,29 +189,29 @@ func TestExtraPermissions(t *testing.T) {
 		}
 	}
 
-	doit("jch", []string{"op", "token", "present"})
-	doit("john", []string{"present"})
+	doit("jch", []string{"op", "token", "present", "message"})
+	doit("john", []string{"present", "message"})
 	doit("james", []string{})
 
 	d.AllowRecording = true
 	d.UnrestrictedTokens = false
 
-	doit("jch", []string{"op", "record", "token", "present"})
-	doit("john", []string{"present"})
+	doit("jch", []string{"op", "record", "token", "present", "message"})
+	doit("john", []string{"present", "message"})
 	doit("james", []string{})
 
 	d.AllowRecording = false
 	d.UnrestrictedTokens = true
 
-	doit("jch", []string{"op", "token", "present"})
-	doit("john", []string{"token", "present"})
+	doit("jch", []string{"op", "token", "present", "message"})
+	doit("john", []string{"token", "present", "message"})
 	doit("james", []string{})
 
 	d.AllowRecording = true
 	d.UnrestrictedTokens = true
 
-	doit("jch", []string{"op", "record", "token", "present"})
-	doit("john", []string{"token", "present"})
+	doit("jch", []string{"op", "record", "token", "present", "message"})
+	doit("john", []string{"token", "present", "message"})
 	doit("james", []string{})
 }
 
diff --git a/rtpconn/webclient.go b/rtpconn/webclient.go
index d0cbf43..12fa330 100644
--- a/rtpconn/webclient.go
+++ b/rtpconn/webclient.go
@@ -1349,6 +1349,10 @@ func setPermissions(g *group.Group, id string, perm string) error {
 		c.permissions = addnew("present", c.permissions)
 	case "unpresent":
 		c.permissions = remove("present", c.permissions)
+	case "shutup":
+		c.permissions = remove("message", c.permissions)
+	case "unshutup":
+		c.permissions = addnew("message", c.permissions)
 	default:
 		return group.UserError("unknown permission")
 	}
@@ -1570,6 +1574,10 @@ func handleClientMessage(c *webClient, m clientMessage) error {
 			return c.error(group.UserError("join a group first"))
 		}
 
+		if !member("message", c.permissions) {
+			return c.error(group.UserError("not authorised"))
+		}
+
 		now := time.Now()
 
 		if m.Type == "chat" {
@@ -1855,7 +1863,7 @@ func handleClientMessage(c *webClient, m clientMessage) error {
 			return c.error(group.UserError("join a group first"))
 		}
 		switch m.Kind {
-		case "op", "unop", "present", "unpresent":
+		case "op", "unop", "present", "unpresent", "shutup", "unshutup":
 			if !member("op", c.permissions) {
 				return c.error(group.UserError("not authorised"))
 			}
diff --git a/static/galene.js b/static/galene.js
index a46625c..4230819 100644
--- a/static/galene.js
+++ b/static/galene.js
@@ -3288,10 +3288,13 @@ function makeToken(template) {
         v["not-before"] = template["not-before"];
     if('permissions' in template)
         v.permissions = template.permissions;
-    else if(serverConnection.permissions.indexOf('present') >= 0)
-        v.permissions = ['present'];
-    else
+    else {
         v.permissions = [];
+        if(serverConnection.permissions.indexOf('present') >= 0)
+            v.permissions.push('present');
+        if(serverConnection.permissions.indexOf('message') >= 0)
+            v.permissions.push('message');
+    }
     serverConnection.groupAction('maketoken', v);
 }
 
@@ -3520,6 +3523,20 @@ commands.unpresent = {
     f: userCommand,
 };
 
+commands.shutup = {
+    parameters: 'user',
+    description: 'revoke the right to send chat messages',
+    predicate: operatorPredicate,
+    f: userCommand,
+};
+
+commands.unshutup = {
+    parameters: 'user',
+    description: 'give the right to send chat messages',
+    predicate: operatorPredicate,
+    f: userCommand,
+};
+
 commands.mute = {
     parameters: 'user',
     description: 'mute a remote user',
diff --git a/token/stateful.go b/token/stateful.go
index 102e535..70339d4 100644
--- a/token/stateful.go
+++ b/token/stateful.go
@@ -106,6 +106,15 @@ func (token *Stateful) Check(host, group string, username *string) (string, []st
 	return user, token.Permissions, nil
 }
 
+func member(v string, l []string) bool {
+	for _, w := range l {
+		if v == w {
+			return true
+		}
+	}
+	return false
+}
+
 // load updates the state from the corresponding file.
 // called locked
 func (state *state) load() (string, error) {
@@ -155,6 +164,12 @@ func (state *state) load() (string, error) {
 			state.fileSize = 0
 			return "", err
 		}
+		// the "message" permission was introduced in Galene 0.9,
+		// so add it to tokens read from disk.  We can remove this
+		// hack in late 2024.
+		if !member("message", t.Permissions) {
+			t.Permissions = append(t.Permissions, "message")
+		}
 		ts[t.Token] = &t
 	}
 	state.tokens = ts
diff --git a/token/stateful_test.go b/token/stateful_test.go
index cce829e..d74407a 100644
--- a/token/stateful_test.go
+++ b/token/stateful_test.go
@@ -48,27 +48,27 @@ func TestStatefulCheck(t *testing.T) {
 		Token:       "token",
 		Group:       "group",
 		Username:    &user,
-		Permissions: []string{"present"},
+		Permissions: []string{"present", "message"},
 		Expires:     &future,
 	}
 	token2 := &Stateful{
 		Token:       "token",
 		Group:       "group",
-		Permissions: []string{"present"},
+		Permissions: []string{"present", "message"},
 		Expires:     &future,
 	}
 	token3 := &Stateful{
 		Token:       "token",
 		Group:       "group",
 		Username:    &user,
-		Permissions: []string{"present"},
+		Permissions: []string{"present", "message"},
 		Expires:     &past,
 	}
 	token4 := &Stateful{
 		Token:       "token",
 		Group:       "group",
 		Username:    &user,
-		Permissions: []string{"present"},
+		Permissions: []string{"present", "message"},
 		Expires:     &future,
 		NotBefore:   &nearFuture,
 	}
@@ -85,27 +85,27 @@ func TestStatefulCheck(t *testing.T) {
 			group:          "group",
 			username:       &user,
 			expUsername:    user,
-			expPermissions: []string{"present"},
+			expPermissions: []string{"present", "message"},
 		},
 		{
 			token:          token1,
 			group:          "group",
 			username:       &user2,
 			expUsername:    user,
-			expPermissions: []string{"present"},
+			expPermissions: []string{"present", "message"},
 		},
 		{
 			token:          token1,
 			group:          "group",
 			expUsername:    user,
-			expPermissions: []string{"present"},
+			expPermissions: []string{"present", "message"},
 		},
 		{
 			token:          token2,
 			group:          "group",
 			username:       &user,
 			expUsername:    "",
-			expPermissions: []string{"present"},
+			expPermissions: []string{"present", "message"},
 		},
 	}
 
@@ -232,14 +232,14 @@ func TestTokenStorage(t *testing.T) {
 			Token:       "tok1",
 			Group:       "test",
 			Username:    &user1,
-			Permissions: []string{"present"},
+			Permissions: []string{"present", "message"},
 			Expires:     &future,
 		},
 		&Stateful{
 			Token:       "tok2",
 			Group:       "test",
 			Username:    &user2,
-			Permissions: []string{"present", "record"},
+			Permissions: []string{"present", "record", "message"},
 			Expires:     &nearFuture,
 			NotBefore:   &past,
 		},
@@ -247,7 +247,7 @@ func TestTokenStorage(t *testing.T) {
 			Token:       "tok3",
 			Group:       "test",
 			Username:    &user3,
-			Permissions: []string{"present"},
+			Permissions: []string{"present", "message"},
 			Expires:     &nearFuture,
 		},
 	}
@@ -327,35 +327,35 @@ func TestExpire(t *testing.T) {
 			Token:       "tok1",
 			Group:       "test",
 			Username:    &user,
-			Permissions: []string{"present"},
+			Permissions: []string{"present", "message"},
 			Expires:     &now,
 		},
 		&Stateful{
 			Token:       "tok2",
 			Group:       "test",
 			Username:    &user,
-			Permissions: []string{"present"},
+			Permissions: []string{"present", "message"},
 			Expires:     &future,
 		},
 		&Stateful{
 			Token:       "tok3",
 			Group:       "test",
 			Username:    &user,
-			Permissions: []string{"present"},
+			Permissions: []string{"present", "message"},
 			Expires:     &now,
 		},
 		&Stateful{
 			Token:       "tok4",
 			Group:       "test",
 			Username:    &user,
-			Permissions: []string{"present"},
+			Permissions: []string{"present", "message"},
 			Expires:     &past,
 		},
 		&Stateful{
 			Token:       "tok5",
 			Group:       "test",
 			Username:    &user,
-			Permissions: []string{"present"},
+			Permissions: []string{"present", "message"},
 			Expires:     &longPast,
 		},
 	}
