From 6a821397b7fa695767b138992e95e069790f31e0 Mon Sep 17 00:00:00 2001
From: Kevin Lyda <kevin@lyda.ie>
Date: Sun, 25 May 2025 20:34:40 +0100
Subject: [PATCH] Add SET [NO]ACCESS and other cleanup

---
 dclish/dclish.go                         |  8 +-
 repl/accounts.go                         |  1 -
 repl/command.go                          | 22 ++++--
 repl/set.go                              | 93 +++++++++++++++++++++++-
 repl/show.go                             |  4 +-
 storage/migrations/1_create_table.up.sql |  2 +
 storage/models.go                        |  5 +-
 storage/queries/standard.sql             | 10 ++-
 storage/standard.sql.go                  | 49 +++++++------
 9 files changed, 150 insertions(+), 44 deletions(-)

diff --git a/dclish/dclish.go b/dclish/dclish.go
index c0cb79b..9f2096e 100644
--- a/dclish/dclish.go
+++ b/dclish/dclish.go
@@ -14,7 +14,8 @@ type ActionFunc func(*Command) error
 // a given command.
 type CompleterFunc func() []string
 
-// Flag is a flag for a command.
+// Flag is a flag for a command. In the future setting a type would make
+// things a little easier.
 type Flag struct {
 	OptArg      bool
 	Value       string
@@ -26,7 +27,9 @@ type Flag struct {
 // Flags is the list of flags.
 type Flags map[string]*Flag
 
-// Command contains the definition of a command, it's flags and subcommands.
+// Command contains the definition of a command, its flags, subcommands
+// and a completer function for the arguments.  The number of args can
+// be limited.
 type Command struct {
 	Flags       Flags
 	Args        []string
@@ -194,7 +197,6 @@ func (c Commands) run(words []string) error {
 		}
 		return cmd.Action(cmd)
 	}
-	// TODO: need to clean this up.
 	args := words[1:]
 	for i := range args {
 		if strings.HasPrefix(args[i], "/") {
diff --git a/repl/accounts.go b/repl/accounts.go
index 6d6abeb..09f0b73 100644
--- a/repl/accounts.go
+++ b/repl/accounts.go
@@ -58,7 +58,6 @@ func ActionUserList(_ *dclish.Command) error {
 		fmt.Printf("ERROR: Failed to list users (%s).\n", err)
 		return nil
 	}
-	// TODO: nicer output for user.
 	for _, u := range userlist {
 		fmt.Printf("%s\n", u)
 	}
diff --git a/repl/command.go b/repl/command.go
index ecfff20..1508942 100644
--- a/repl/command.go
+++ b/repl/command.go
@@ -1130,19 +1130,29 @@ characteristics of the BULLETIN Utility.
 The following options are available:
 
   ACCESS           ALWAYS           BRIEF            DEFAULT_EXPIRE
-  EXPIRE_LIMIT     FOLDER           NOALWAYS         NOBRIEF
-  NOPROMPT_EXPIRE  NOREADNEW        NOSHOWNEW        NOSYSTEM
-  PROMPT_EXPIRE    READNEW          SHOWNEW          SYSTEM
+  EXPIRE_LIMIT     FOLDER           NOACCESS         NOALWAYS
+  NOBRIEF          NOPROMPT_EXPIRE  NOREADNEW        NOSHOWNEW
+  NOSYSTEM         PROMPT_EXPIRE    READNEW          SHOWNEW
+  SYSTEM
 `,
 		Action: ActionSet,
 		Commands: dclish.Commands{
+			"NOACCESS": {
+				Description: `This removes access for users.
+
+  Format:
+	  SET NOACCESS id-name [folder-name]`,
+				MinArgs: 1,
+				MaxArgs: 2,
+				Action:  ActionSetNoaccess,
+			},
 			"ACCESS": {
 				Description: `Controls  access  to  a  private  folder.   A private folder can only be
 selected by users who have been granted access.  Only the owner of  that
 folder is allowed to grant access.
 
   Format:
-    SET [NO]ACCESS id-name [folder-name]
+    SET ACCESS id-name [folder-name]
 
 The id-name can be one or more ids from the system Rights  Database  for
 which  access  is  being  modified.   It  can  also be a file name which
@@ -1169,7 +1179,9 @@ messages,  and thus will not be able to set any login flags.  (NOTE:  If
 such a user selects such a folder and then uses SET ACCESS to grant  him
 or  herself  access,  the user must reselect the folder in order for the
 new access to take affect in order to be able to set login flags.)`,
-				Action: ActionSetAccess,
+				MinArgs: 0,
+				MaxArgs: 2,
+				Action:  ActionSetAccess,
 				Flags: dclish.Flags{
 					"/ALL": {
 						Description: `  Specifies that access to the folder  is granted to all users. If /READ
diff --git a/repl/set.go b/repl/set.go
index dc09468..249452d 100644
--- a/repl/set.go
+++ b/repl/set.go
@@ -60,9 +60,92 @@ func ActionSet(cmd *dclish.Command) error {
 	return nil
 }
 
+// ActionSetNoaccess handles the `SET ACCESS` command.
+func ActionSetNoaccess(cmd *dclish.Command) error {
+	ctx := storage.Context()
+	login := cmd.Args[0]
+	folder := this.Folder
+	if len(cmd.Args) == 2 {
+		folder = folders.FindFolder(cmd.Args[1])
+		if folder.Name == "" {
+			return errors.New("Folder not found")
+		}
+	}
+	if this.User.Admin == 0 || folder.Owner != this.User.Login {
+		return errors.New("Must be an admin or folder owner")
+	}
+	this.Q.DeleteFolderAccess(ctx,
+		storage.DeleteFolderAccessParams{
+			Login:  login,
+			Folder: folder.Name,
+		})
+	return nil
+}
+
 // ActionSetAccess handles the `SET ACCESS` command.
-func ActionSetAccess(_ *dclish.Command) error {
-	fmt.Println("TODO: implement ActionSetAccess.")
+func ActionSetAccess(cmd *dclish.Command) error {
+	ctx := storage.Context()
+	optAll := cmd.Flags["/ALL"].Set
+	if optAll {
+		if cmd.Flags["/ALL"].Value != "true" {
+			return errors.New("Flag '/NOALL' not recognised")
+		}
+	}
+	optRead := cmd.Flags["/READ"].Set
+	if optRead {
+		if cmd.Flags["/READ"].Value != "true" {
+			return errors.New("Flag '/READ' not recognised")
+		}
+	}
+	if optAll {
+		if len(cmd.Args) > 1 {
+			return errors.New("Too many arguments for /ALL")
+		}
+		folder := this.Folder
+		if len(cmd.Args) == 1 {
+			folder = folders.FindFolder(cmd.Args[0])
+			if folder.Name == "" {
+				return errors.New("Folder not found")
+			}
+		}
+		if this.User.Admin == 0 || folder.Owner != this.User.Login {
+			return errors.New("Must be an admin or folder owner")
+		}
+		visibility := int64(2)
+		if optRead {
+			visibility = 1
+		}
+		this.Q.UpdateFolderVisibility(ctx,
+			storage.UpdateFolderVisibilityParams{
+				Visibility: visibility,
+				Name:       folder.Name,
+			})
+		return nil
+	}
+	if len(cmd.Args) == 0 {
+		return errors.New("Must supply a user login to set access")
+	}
+	login := cmd.Args[0]
+	folder := this.Folder
+	if len(cmd.Args) == 2 {
+		folder = folders.FindFolder(cmd.Args[1])
+		if folder.Name == "" {
+			return errors.New("Folder not found")
+		}
+	}
+	if this.User.Admin == 0 || folder.Owner != this.User.Login {
+		return errors.New("Must be an admin or folder owner")
+	}
+	visibility := int64(2)
+	if optRead {
+		visibility = 1
+	}
+	this.Q.UpdateFolderAccess(ctx,
+		storage.UpdateFolderAccessParams{
+			Login:      login,
+			Folder:     folder.Name,
+			Visibility: visibility,
+		})
 	return nil
 }
 
@@ -167,7 +250,6 @@ func ActionSetSystem(_ *dclish.Command) error {
 	if this.User.Admin == 0 {
 		return errors.New("You are not an admin")
 	}
-	// TODO: parse flags and args.
 	ctx := storage.Context()
 	this.Q.UpdateFolderSystem(ctx, storage.UpdateFolderSystemParams{
 		System: 1,
@@ -181,7 +263,10 @@ func ActionSetNosystem(_ *dclish.Command) error {
 	if this.User.Admin == 0 {
 		return errors.New("You are not an admin")
 	}
-	// TODO: parse flags and args.
+	if this.Folder.Name == "GENERAL" {
+		fmt.Println("Can't remove SYSTEM from the GENERAL folder.")
+		return nil
+	}
 	ctx := storage.Context()
 	this.Q.UpdateFolderSystem(ctx, storage.UpdateFolderSystemParams{
 		System: 1,
diff --git a/repl/show.go b/repl/show.go
index d4a6b00..e3852ee 100644
--- a/repl/show.go
+++ b/repl/show.go
@@ -35,7 +35,8 @@ func ActionShowFlags(_ *dclish.Command) error {
 	return nil
 }
 
-// ActionShowFolder handles the `SHOW FOLDER` command.
+// ActionShowFolder handles the `SHOW FOLDER` command.  This is based on
+// `SHOW_FOLDER` in bulletin5.for.
 func ActionShowFolder(cmd *dclish.Command) error {
 	folder := this.Folder
 	if len(cmd.Args) == 1 {
@@ -75,7 +76,6 @@ func ActionShowFolder(cmd *dclish.Command) error {
 	if folder.System != 0 {
 		fmt.Println("  SYSTEM has been set.")
 	}
-	// TODO: Review SHOW_FOLDER in bulletin5.for.
 	if folder.Always != 0 {
 		fmt.Println("  ALWAYS has been set.")
 	}
diff --git a/storage/migrations/1_create_table.up.sql b/storage/migrations/1_create_table.up.sql
index e81b148..7798aad 100644
--- a/storage/migrations/1_create_table.up.sql
+++ b/storage/migrations/1_create_table.up.sql
@@ -146,6 +146,8 @@ CREATE TABLE folder_access (
               ON DELETE CASCADE ON UPDATE CASCADE NOT NULL,
   folder      VARCHAR(25) REFERENCES folders(name)
               ON DELETE CASCADE ON UPDATE CASCADE NOT NULL,
+  --- read-only=1, read-write=2
+  visibility  INT    DEFAULT 1 NOT NULL,
   PRIMARY KEY (login, folder)
 ) WITHOUT ROWID;
 
diff --git a/storage/models.go b/storage/models.go
index fd2c0cd..54058ee 100644
--- a/storage/models.go
+++ b/storage/models.go
@@ -29,8 +29,9 @@ type Folder struct {
 }
 
 type FolderAccess struct {
-	Login  string
-	Folder string
+	Login      string
+	Folder     string
+	Visibility int64
 }
 
 type FolderConfig struct {
diff --git a/storage/queries/standard.sql b/storage/queries/standard.sql
index 2b475b6..34ef417 100644
--- a/storage/queries/standard.sql
+++ b/storage/queries/standard.sql
@@ -55,14 +55,16 @@ SELECT * FROM marks WHERE folder = ? AND login = ? AND msgid = ?;
 -- name: DeleteMark :exec
 DELETE FROM marks WHERE folder = ? AND login = ? AND msgid = ?;
 
--- name: AddFolderAccess :exec
-INSERT INTO folder_access (login, folder) VALUES (?, ?);
+-- name: UpdateFolderAccess :exec
+INSERT INTO folder_access (login, folder, visibility) VALUES (?1, ?2, ?3)
+  ON CONFLICT(login, folder) DO UPDATE
+  SET visibility = ?3;
 
 -- name: ListFolderAccess :many
-SELECT * FROM folder_access;
+SELECT * FROM folder_access WHERE folder = ?;
 
 -- name: GetFolderAccess :one
-SELECT * FROM folder_access WHERE login = ? AND folder = ?;
+SELECT visibility FROM folder_access WHERE login = ? AND folder = ?;
 
 -- name: DeleteFolderAccess :exec
 DELETE FROM folder_access WHERE login = ? AND folder = ?;
diff --git a/storage/standard.sql.go b/storage/standard.sql.go
index 47ad5d4..f26c1e2 100644
--- a/storage/standard.sql.go
+++ b/storage/standard.sql.go
@@ -18,20 +18,6 @@ func (q *Queries) AddFolder(ctx context.Context, name string) error {
 	return err
 }
 
-const addFolderAccess = `-- name: AddFolderAccess :exec
-INSERT INTO folder_access (login, folder) VALUES (?, ?)
-`
-
-type AddFolderAccessParams struct {
-	Login  string
-	Folder string
-}
-
-func (q *Queries) AddFolderAccess(ctx context.Context, arg AddFolderAccessParams) error {
-	_, err := q.db.ExecContext(ctx, addFolderAccess, arg.Login, arg.Folder)
-	return err
-}
-
 const addFolderConfig = `-- name: AddFolderConfig :exec
 INSERT INTO folder_configs (login, folder) VALUES (?, ?)
 `
@@ -203,7 +189,7 @@ func (q *Queries) GetFolder(ctx context.Context, name string) (Folder, error) {
 }
 
 const getFolderAccess = `-- name: GetFolderAccess :one
-SELECT login, folder FROM folder_access WHERE login = ? AND folder = ?
+SELECT visibility FROM folder_access WHERE login = ? AND folder = ?
 `
 
 type GetFolderAccessParams struct {
@@ -211,11 +197,11 @@ type GetFolderAccessParams struct {
 	Folder string
 }
 
-func (q *Queries) GetFolderAccess(ctx context.Context, arg GetFolderAccessParams) (FolderAccess, error) {
+func (q *Queries) GetFolderAccess(ctx context.Context, arg GetFolderAccessParams) (int64, error) {
 	row := q.db.QueryRowContext(ctx, getFolderAccess, arg.Login, arg.Folder)
-	var i FolderAccess
-	err := row.Scan(&i.Login, &i.Folder)
-	return i, err
+	var visibility int64
+	err := row.Scan(&visibility)
+	return visibility, err
 }
 
 const getFolderConfig = `-- name: GetFolderConfig :one
@@ -318,11 +304,11 @@ func (q *Queries) GetSystem(ctx context.Context) (System, error) {
 }
 
 const listFolderAccess = `-- name: ListFolderAccess :many
-SELECT login, folder FROM folder_access
+SELECT login, folder, visibility FROM folder_access WHERE folder = ?
 `
 
-func (q *Queries) ListFolderAccess(ctx context.Context) ([]FolderAccess, error) {
-	rows, err := q.db.QueryContext(ctx, listFolderAccess)
+func (q *Queries) ListFolderAccess(ctx context.Context, folder string) ([]FolderAccess, error) {
+	rows, err := q.db.QueryContext(ctx, listFolderAccess, folder)
 	if err != nil {
 		return nil, err
 	}
@@ -330,7 +316,7 @@ func (q *Queries) ListFolderAccess(ctx context.Context) ([]FolderAccess, error)
 	var items []FolderAccess
 	for rows.Next() {
 		var i FolderAccess
-		if err := rows.Scan(&i.Login, &i.Folder); err != nil {
+		if err := rows.Scan(&i.Login, &i.Folder, &i.Visibility); err != nil {
 			return nil, err
 		}
 		items = append(items, i)
@@ -600,3 +586,20 @@ func (q *Queries) SetSystem(ctx context.Context, arg SetSystemParams) error {
 	_, err := q.db.ExecContext(ctx, setSystem, arg.Name, arg.DefaultExpire, arg.ExpireLimit)
 	return err
 }
+
+const updateFolderAccess = `-- name: UpdateFolderAccess :exec
+INSERT INTO folder_access (login, folder, visibility) VALUES (?1, ?2, ?3)
+  ON CONFLICT(login, folder) DO UPDATE
+  SET visibility = ?3
+`
+
+type UpdateFolderAccessParams struct {
+	Login      string
+	Folder     string
+	Visibility int64
+}
+
+func (q *Queries) UpdateFolderAccess(ctx context.Context, arg UpdateFolderAccessParams) error {
+	_, err := q.db.ExecContext(ctx, updateFolderAccess, arg.Login, arg.Folder, arg.Visibility)
+	return err
+}
-- 
GitLab