diff --git a/batch/batch.go b/batch/batch.go index e22082baa387bc879e0f31f5921e2363c1e63664..f17eb5c4b9df8cd73a5879fae5c0e7fa09a84d46 100644 --- a/batch/batch.go +++ b/batch/batch.go @@ -79,8 +79,7 @@ the following files: * ~/.ssh/authorized_keys - key lines to let users connect to BULLETIN To complete the installation, please enter the login, name and ssh key for -the first user. -`) +the first user.`) login, err := ask.GetLine("Enter login of initial user: ") login = strings.ToUpper(login) ask.CheckErr(err) @@ -107,8 +106,9 @@ the first user. for i := range seedMsgs { if !userCreated[seedMsgs[i].Login] { _, err = q.AddUser(ctx, storage.AddUserParams{ - Login: seedMsgs[i].Login, - Name: seedMsgs[i].Name, + Login: seedMsgs[i].Login, + Name: seedMsgs[i].Name, + Disabled: 1, }) ask.CheckErr(err) userCreated[seedMsgs[i].Login] = true diff --git a/dclish/dclish.go b/dclish/dclish.go index 76a38a2c05ff7f779817393ff57a712b135bc5bb..7736360d7616b5ebbf3465acd6d80425b7ef2707 100644 --- a/dclish/dclish.go +++ b/dclish/dclish.go @@ -16,6 +16,7 @@ type Flag struct { Value string Default string Description string + Set bool } // Flags is the list of flags. @@ -177,6 +178,7 @@ func (c Commands) run(words []string) error { } for flg := range cmd.Flags { cmd.Flags[flg].Value = cmd.Flags[flg].Default + cmd.Flags[flg].Set = false } cmd.Args = []string{} @@ -196,7 +198,7 @@ func (c Commands) run(words []string) error { if assigned { wordup = strings.ToUpper(flag) } else { - wordup = args[i] + wordup = strings.ToUpper(args[i]) } toggleValue := "true" flg, ok := cmd.Flags[wordup] @@ -213,6 +215,11 @@ func (c Commands) run(words []string) error { fmt.Printf("ERROR: Flag '%s' is a toggle.\n", args[i]) return nil } + if flg.Set { + fmt.Printf("ERROR: Flag '%s' is already set.\n", args[i]) + return nil + } + flg.Set = true if flg.OptArg { if assigned { flg.Value = strings.Trim(val, "\"'") diff --git a/repl/command.go b/repl/command.go index 493a12b5bd37e8e2de0fe670e0b2f4887be05440..0dc244ee651ada1004e217dc85ac7d5129e4d9f8 100644 --- a/repl/command.go +++ b/repl/command.go @@ -1205,83 +1205,6 @@ characteristics of the BULLETIN Utility. SET option`, Action: ActionSet, Commands: dclish.Commands{ - "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] - -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 -contains a list of ids. For more information concerning usage of -private folders, see HELP CREATE /PRIVATE. NOTE: Access is created via -ACLs. If a user's process privileges are set to override ACLs, that -user will be able to access the folder even if access has not been -granted. - -It is suggested that if you plan on granting access to many users, that -you create an id using the AUTHORIZE utility and then use the SET ACCESS -command to grant access to that id. Then, you can use the GRANT/ID -command in AUTHORIZE to grant the id to users, and this will give those -users access to the folder. This is preferred because of problems with -running into system quota when checking for acls on a file with a large -amount of acls. It is also means that you don't have to remember to -remove the access for that user from a folder if that user is removed -from the system. - -A user with BULLETIN privileges (see HELP SET PRIV) will be able to -select a protected folder regardless of the access settings. However, a -user without explicit access will not receive login notifications of new -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.) - -The id-name can be one or more ids contained in the system Rights -Database. This includes usernames and UICs. A UIC that contains a -comma must be enclosed in quotes. UICs can contain wildcards, i.e. -"[130,*]". Note that by default, a process is given the process rights -id SYS$NODE_nodename, where nodename is the decnet nodename. Thus, by -specifing this id, a folder can be restricted to a specific node, which -is useful when the folder is shared among nodes in a cluster. - -Alternatively, the id-name can be a filename which contains a list of -ids. The filename should be preceeded by a "@". If the suffix is not -specified, it will be assumed that the suffix is ".DIS" . - -Warning - - If a user logs in after a private folder has been created but before - being given access, and then is given access, any defaults that the - folder has, i.e. /BRIEF, /READNEW, & /NOTIFY, will not be set for that - user. This is because if the id is not a username, it becomes an - extremely lengthy operation to check each user to see if have that id - assigned to them. The alternative is to set the defaults for all users - after every SET ACCESS, but that might cause problems with users who - have manually reset those defaults. The correct solution requires a - large programming modification, which will be done in a later version.`, - MinArgs: 1, - MaxArgs: 1, - Flags: dclish.Flags{ - "/ALL": { - Description: ` Specifies that access to the folder is granted to all users. If /READ - is not specified, the folder will no longer be private. If /READ is - specified, all users will have read access, but only privileged users - will have write access (of course non-privileged users can gain access - via a later SET ACCESS command.) - - Format: - SET ACCESS /ALL[=folder-name]`, - OptArg: true, - }, - "/READ": { - Description: ` Specifies that access to the folder will be limited to being able to - read the messages.`, - }, - }, - }, "ALWAYS": { Description: `Specifies that the selected folder has the ALWAYS attribute. This causes messages in the folder to be displayed differently when logging @@ -1665,6 +1588,7 @@ have done this.`, Specifies to display the latest message that was read by the user(s) for the specified foldername. If the foldername is not specified, the selected folder will be used.`, + OptArg: true, }, "/SINCE": { Description: `/SINCE=[date] diff --git a/repl/show.go b/repl/show.go index a583496e0faa9a4b30f89858567e22d4017d3472..a0ce307b2a70d96e2a271be0ef068f91d7c596bd 100644 --- a/repl/show.go +++ b/repl/show.go @@ -3,6 +3,7 @@ package repl import ( "fmt" + "strings" "github.com/carlmjohnson/versioninfo" @@ -127,33 +128,128 @@ func ActionShowPrivileges(_ *dclish.Command) error { // ActionShowUser handles the `SHOW USER` command. func ActionShowUser(cmd *dclish.Command) error { - showAll := false - if cmd.Flags["/ALL"].Value == "true" { - showAll = true - // TODO: Check permissions. - fmt.Println("ERROR: No privs to use command.") - return nil + // Parse the options. + login := this.User.Login + if len(cmd.Args) == 1 { + if this.User.Admin == 0 { + fmt.Println("ERROR: You are not an admin.") + return nil + } + login = strings.ToUpper(cmd.Args[0]) + } + if cmd.Flags["/ALL"].Set { + if this.User.Admin == 0 { + fmt.Println("ERROR: You are not an admin.") + return nil + } + if len(cmd.Args) == 1 { + fmt.Println("ERROR: You can't specify a user with /ALL.") + return nil + } } - showLogin := false + var showLogin int64 if cmd.Flags["/LOGIN"].Value == "true" { - // TODO: Check permissions. - showLogin = true + if this.User.Admin == 0 { + fmt.Println("ERROR: You are not an admin.") + return nil + } + showLogin = 1 } folder := this.Folder - if cmd.Flags["/FOLDER"].Value != "" { - // TODO: Check permissions. - folder = folders.FindFolder(cmd.Flags["/FOLDER"].Value) + if cmd.Flags["/FOLDER"].Set { + fmt.Printf("%v\n", cmd.Flags["/FOLDER"].Value) + if cmd.Flags["/FOLDER"].Value != "" { + folder = folders.FindFolder(cmd.Flags["/FOLDER"].Value) + if folder.Name == "" { + fmt.Println("ERROR: Folder does not exist.") + return nil + } + } } - if cmd.Flags["/FOLDER"].Value != "" { - since, err := ParseDate(cmd.Flags["/FOLDER"].Value) + /* + TODO: need to add this. + var since time.Time + var err error + if cmd.Flags["/SINCE"].Value != "" { + if cmd.Flags["/LOGIN"].Set || cmd.Flags["/FOLDER"].Set { + fmt.Println("ERROR: Must set /[NO]LOGIN or /FOLDER.") + return nil + } + since, err = ParseDate(cmd.Flags["/SINCE"].Value) + if err != nil { + fmt.Println("ERROR: Invalid date specified.") + return nil + } + } + */ + + // Actually do the thing. + ctx := storage.Context() + if cmd.Flags["/LOGIN"].Set && cmd.Flags["/FOLDER"].Set { + rows, err := this.Q.GetLastReadByEnabled(ctx, + storage.GetLastReadByEnabledParams{ + Folder: folder.Name, + Disabled: showLogin, + }) + if err != nil { + fmt.Printf("ERROR: Failed to get list (%s).\n", err) + return nil + } + fmt.Println("User Folder Message #") + for _, r := range rows { + fmt.Printf("%-12s %-25s % 4d\n", r.Author, folder.Name, r.ID) + } + } else if cmd.Flags["/ALL"].Set && cmd.Flags["/FOLDER"].Set { + rows, err := this.Q.GetLastRead(ctx, folder.Name) + if err != nil { + fmt.Printf("ERROR: Failed to get list (%s).\n", err) + return nil + } + fmt.Println("User Folder Message #") + for _, r := range rows { + fmt.Printf("%-12s %-25s % 4d\n", r.Author, folder.Name, r.ID) + } + } else if cmd.Flags["/FOLDER"].Set { + r, err := this.Q.GetLastReadByUser(ctx, + storage.GetLastReadByUserParams{ + Folder: folder.Name, + Author: login, + }) + if err != nil { + fmt.Printf("ERROR: Failed to get list (%s).\n", err) + return nil + } + fmt.Println("User Folder Message #") + fmt.Printf("%-12s %-25s % 4d\n", r.Author, folder.Name, r.ID) + } else if cmd.Flags["/LOGIN"].Set { + rows, err := this.Q.GetLastLoginByEnabled(ctx, showLogin) + if err != nil { + fmt.Printf("ERROR: Failed to get list (%s).\n", err) + return nil + } + fmt.Println("User Last Login") + for _, r := range rows { + fmt.Printf("%-12s %s\n", r.Login, r.LastLogin.Format("2006-01-02 15:04:05")) + } + } else if cmd.Flags["/ALL"].Set { + rows, err := this.Q.GetLastLogin(ctx) + if err != nil { + fmt.Printf("ERROR: Failed to get list (%s).\n", err) + return nil + } + fmt.Println("User Last Login") + for _, r := range rows { + fmt.Printf("%-12s %s\n", r.Login, r.LastLogin.Format("2006-01-02 15:04:05")) + } + } else { + r, err := this.Q.GetLastLoginByLogin(ctx, login) if err != nil { - fmt.Println("ERROR: Invalid date specified.") + fmt.Printf("ERROR: Failed to get list (%s).\n", err) return nil } - fmt.Printf("TODO: select messages since %s.\n", since.Format("2006-05-04")) + fmt.Println("User Last Login") + fmt.Printf("%-12s %s\n", r.Login, r.LastLogin.Format("2006-01-02 15:04:05")) } - fmt.Println("TODO: implement ActionShowUser.") - fmt.Printf("TODO: %t %t %s.\n", showAll, showLogin, folder.Name) return nil } diff --git a/storage/display.go b/storage/display.go index 1ac380fb47a2586568427dc019880afcfb0de80d..7972da1ed16f4619b96c496c5e852b658c63bed9 100644 --- a/storage/display.go +++ b/storage/display.go @@ -43,7 +43,7 @@ func (u User) String() string { u.Moderator, u.Alert, u.Disabled, - u.LastLogin.Format("06-05-04 15:02:01")) + u.LastLogin.Format("06-01-02 15:04:05")) } // String displays a folder (mainly used for debugging). diff --git a/storage/messages.sql.go b/storage/messages.sql.go index bdec2bb9fe67e29f4b51dd4b9dd507bc4779729f..a916720e298ecb15511515134c9fa06f54b16edb 100644 --- a/storage/messages.sql.go +++ b/storage/messages.sql.go @@ -50,6 +50,104 @@ func (q *Queries) DeleteAllMessages(ctx context.Context, folder string) error { return err } +const getLastRead = `-- name: GetLastRead :many +SELECT CAST(MAX(m.id) AS INT) AS id, m.author FROM messages AS m, users AS u +WHERE folder = ? AND u.login == m.author +GROUP BY m.author +ORDER BY m.author +` + +type GetLastReadRow struct { + ID int64 + Author string +} + +// - TODO: These get the max message written, not read. Leaving for now; easier to test. +func (q *Queries) GetLastRead(ctx context.Context, folder string) ([]GetLastReadRow, error) { + rows, err := q.db.QueryContext(ctx, getLastRead, folder) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetLastReadRow + for rows.Next() { + var i GetLastReadRow + if err := rows.Scan(&i.ID, &i.Author); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getLastReadByEnabled = `-- name: GetLastReadByEnabled :many +SELECT CAST(MAX(m.id) AS INT) AS id, m.author FROM messages AS m, users AS u +WHERE folder = ? AND u.login == m.author AND u.disabled = ? +GROUP BY m.author +ORDER BY m.author +` + +type GetLastReadByEnabledParams struct { + Folder string + Disabled int64 +} + +type GetLastReadByEnabledRow struct { + ID int64 + Author string +} + +func (q *Queries) GetLastReadByEnabled(ctx context.Context, arg GetLastReadByEnabledParams) ([]GetLastReadByEnabledRow, error) { + rows, err := q.db.QueryContext(ctx, getLastReadByEnabled, arg.Folder, arg.Disabled) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetLastReadByEnabledRow + for rows.Next() { + var i GetLastReadByEnabledRow + if err := rows.Scan(&i.ID, &i.Author); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getLastReadByUser = `-- name: GetLastReadByUser :one +SELECT CAST(MAX(m.id) AS INT) AS id, m.author FROM messages AS m, users AS u +WHERE folder = ? AND u.login == m.author AND m.author = ? +` + +type GetLastReadByUserParams struct { + Folder string + Author string +} + +type GetLastReadByUserRow struct { + ID int64 + Author string +} + +func (q *Queries) GetLastReadByUser(ctx context.Context, arg GetLastReadByUserParams) (GetLastReadByUserRow, error) { + row := q.db.QueryRowContext(ctx, getLastReadByUser, arg.Folder, arg.Author) + var i GetLastReadByUserRow + err := row.Scan(&i.ID, &i.Author) + return i, err +} + const lastMsgidIgnoringSeen = `-- name: LastMsgidIgnoringSeen :one SELECT CAST(MAX(id) AS INT) FROM messages AS m WHERE m.folder = ?1 ` diff --git a/storage/queries/messages.sql b/storage/queries/messages.sql index e9bd74d0daadec4096f9a000e73977affb3a744e..aaf975c1eeeddc40ca5979125a6f67094d84bf41 100644 --- a/storage/queries/messages.sql +++ b/storage/queries/messages.sql @@ -37,3 +37,20 @@ DELETE FROM messages WHERE folder = ?; -- name: LastMsgidIgnoringSeen :one SELECT CAST(MAX(id) AS INT) FROM messages AS m WHERE m.folder = ?1; + +--- TODO: These get the max message written, not read. Leaving for now; easier to test. +-- name: GetLastRead :many +SELECT CAST(MAX(m.id) AS INT) AS id, m.author FROM messages AS m, users AS u +WHERE folder = ? AND u.login == m.author +GROUP BY m.author +ORDER BY m.author; + +-- name: GetLastReadByEnabled :many +SELECT CAST(MAX(m.id) AS INT) AS id, m.author FROM messages AS m, users AS u +WHERE folder = ? AND u.login == m.author AND u.disabled = ? +GROUP BY m.author +ORDER BY m.author; + +-- name: GetLastReadByUser :one +SELECT CAST(MAX(m.id) AS INT) AS id, m.author FROM messages AS m, users AS u +WHERE folder = ? AND u.login == m.author AND m.author = ?; diff --git a/storage/queries/users.sql b/storage/queries/users.sql index 880ddcba85f55b4df96d1a92ba2a24ce176d34e9..d87b01581a25271c33d11dd8a5e0146593dcd12c 100644 --- a/storage/queries/users.sql +++ b/storage/queries/users.sql @@ -2,7 +2,7 @@ SELECT * FROM users WHERE login = ?; -- name: AddUser :one -INSERT INTO users (login, name, admin) VALUES (?, ?, ?) +INSERT INTO users (login, name, admin, disabled, last_login) VALUES (?, ?, ?, ?, ?) RETURNING *; -- name: IsUserAdmin :one @@ -23,5 +23,15 @@ UPDATE users SET name = ? WHERE login = ? AND login != 'SYSTEM'; -- name: UpdateUserMod :exec UPDATE users SET moderator = ? WHERE login = ? AND login != 'SYSTEM'; --- name: UpdateUserLastLogin :exec -UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE login = ? AND login != 'SYSTEM'; +-- name: UpdateUserLastLogin :one +UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE login = ? AND login != 'SYSTEM' +RETURNING last_login; + +-- name: GetLastLogin :many +SELECT login, last_login FROM users ORDER BY login; + +-- name: GetLastLoginByLogin :one +SELECT login, last_login FROM users WHERE login = ? ORDER BY login; + +-- name: GetLastLoginByEnabled :many +SELECT login, last_login FROM users WHERE disabled = ? ORDER BY login; diff --git a/storage/users.sql.go b/storage/users.sql.go index 79a138ade0b0731243387eec91bf49af551f3ddd..75f923ca32b1cc8c1cb0f3cfd66a085b02eaacfd 100644 --- a/storage/users.sql.go +++ b/storage/users.sql.go @@ -7,21 +7,30 @@ package storage import ( "context" + "time" ) const addUser = `-- name: AddUser :one -INSERT INTO users (login, name, admin) VALUES (?, ?, ?) +INSERT INTO users (login, name, admin, disabled, last_login) VALUES (?, ?, ?, ?, ?) RETURNING login, name, admin, moderator, alert, disabled, last_login, create_at, update_at ` type AddUserParams struct { - Login string - Name string - Admin int64 + Login string + Name string + Admin int64 + Disabled int64 + LastLogin time.Time } func (q *Queries) AddUser(ctx context.Context, arg AddUserParams) (User, error) { - row := q.db.QueryRowContext(ctx, addUser, arg.Login, arg.Name, arg.Admin) + row := q.db.QueryRowContext(ctx, addUser, + arg.Login, + arg.Name, + arg.Admin, + arg.Disabled, + arg.LastLogin, + ) var i User err := row.Scan( &i.Login, @@ -37,6 +46,86 @@ func (q *Queries) AddUser(ctx context.Context, arg AddUserParams) (User, error) return i, err } +const getLastLogin = `-- name: GetLastLogin :many +SELECT login, last_login FROM users ORDER BY login +` + +type GetLastLoginRow struct { + Login string + LastLogin time.Time +} + +func (q *Queries) GetLastLogin(ctx context.Context) ([]GetLastLoginRow, error) { + rows, err := q.db.QueryContext(ctx, getLastLogin) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetLastLoginRow + for rows.Next() { + var i GetLastLoginRow + if err := rows.Scan(&i.Login, &i.LastLogin); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getLastLoginByEnabled = `-- name: GetLastLoginByEnabled :many +SELECT login, last_login FROM users WHERE disabled = ? ORDER BY login +` + +type GetLastLoginByEnabledRow struct { + Login string + LastLogin time.Time +} + +func (q *Queries) GetLastLoginByEnabled(ctx context.Context, disabled int64) ([]GetLastLoginByEnabledRow, error) { + rows, err := q.db.QueryContext(ctx, getLastLoginByEnabled, disabled) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetLastLoginByEnabledRow + for rows.Next() { + var i GetLastLoginByEnabledRow + if err := rows.Scan(&i.Login, &i.LastLogin); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getLastLoginByLogin = `-- name: GetLastLoginByLogin :one +SELECT login, last_login FROM users WHERE login = ? ORDER BY login +` + +type GetLastLoginByLoginRow struct { + Login string + LastLogin time.Time +} + +func (q *Queries) GetLastLoginByLogin(ctx context.Context, login string) (GetLastLoginByLoginRow, error) { + row := q.db.QueryRowContext(ctx, getLastLoginByLogin, login) + var i GetLastLoginByLoginRow + err := row.Scan(&i.Login, &i.LastLogin) + return i, err +} + const getUser = `-- name: GetUser :one SELECT login, name, admin, moderator, alert, disabled, last_login, create_at, update_at FROM users WHERE login = ? ` @@ -111,13 +200,16 @@ func (q *Queries) UpdateUserDisabled(ctx context.Context, arg UpdateUserDisabled return err } -const updateUserLastLogin = `-- name: UpdateUserLastLogin :exec +const updateUserLastLogin = `-- name: UpdateUserLastLogin :one UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE login = ? AND login != 'SYSTEM' +RETURNING last_login ` -func (q *Queries) UpdateUserLastLogin(ctx context.Context, login string) error { - _, err := q.db.ExecContext(ctx, updateUserLastLogin, login) - return err +func (q *Queries) UpdateUserLastLogin(ctx context.Context, login string) (time.Time, error) { + row := q.db.QueryRowContext(ctx, updateUserLastLogin, login) + var last_login time.Time + err := row.Scan(&last_login) + return last_login, err } const updateUserMod = `-- name: UpdateUserMod :exec diff --git a/this/this.go b/this/this.go index 23cf5c3bc3cdabc3418024313e7bcc3885b6c11b..0a14a0740f5b75584c03420338b8fcf6be10112b 100644 --- a/this/this.go +++ b/this/this.go @@ -67,6 +67,8 @@ func StartThis(login string) error { return err } fmt.Println("User successfully created. Enjoy!") + } else { + User.LastLogin, _ = Q.UpdateUserLastLogin(ctx, User.Login) } if User.Disabled == 1 { return errors.New("User is disabled")