2022-12-25 16:41:38 +00:00
package user
2022-01-23 05:02:16 +00:00
2022-01-31 16:44:58 +00:00
import (
2022-12-26 03:29:55 +00:00
"database/sql"
"encoding/json"
2022-01-31 16:44:58 +00:00
"errors"
2022-12-26 03:29:55 +00:00
"fmt"
_ "github.com/mattn/go-sqlite3" // SQLite driver
"golang.org/x/crypto/bcrypt"
"heckel.io/ntfy/log"
"heckel.io/ntfy/util"
"strings"
"sync"
"time"
2022-01-31 16:44:58 +00:00
)
2022-01-23 05:02:16 +00:00
2022-12-26 03:29:55 +00:00
const (
tokenLength = 32
bcryptCost = 10
intentionalSlowDownHash = "$2a$10$YFCQvqQDwIIwnJM1xkAYOeih0dg17UVGanaTStnrSzC8NCWxcLDwy" // Cost should match bcryptCost
userStatsQueueWriterInterval = 33 * time . Second
userTokenExpiryDuration = 72 * time . Hour
)
// Manager-related queries
const (
2022-12-29 18:08:47 +00:00
createTablesQueriesNoTx = `
2022-12-26 03:29:55 +00:00
CREATE TABLE IF NOT EXISTS plan (
id INT NOT NULL ,
code TEXT NOT NULL ,
messages_limit INT NOT NULL ,
emails_limit INT NOT NULL ,
attachment_file_size_limit INT NOT NULL ,
attachment_total_size_limit INT NOT NULL ,
PRIMARY KEY ( id )
) ;
CREATE TABLE IF NOT EXISTS user (
id INTEGER PRIMARY KEY AUTOINCREMENT ,
plan_id INT ,
user TEXT NOT NULL ,
pass TEXT NOT NULL ,
role TEXT NOT NULL ,
messages INT NOT NULL DEFAULT ( 0 ) ,
emails INT NOT NULL DEFAULT ( 0 ) ,
settings JSON ,
FOREIGN KEY ( plan_id ) REFERENCES plan ( id )
) ;
CREATE UNIQUE INDEX idx_user ON user ( user ) ;
CREATE TABLE IF NOT EXISTS user_access (
user_id INT NOT NULL ,
topic TEXT NOT NULL ,
read INT NOT NULL ,
write INT NOT NULL ,
PRIMARY KEY ( user_id , topic ) ,
FOREIGN KEY ( user_id ) REFERENCES user ( id ) ON DELETE CASCADE
) ;
CREATE TABLE IF NOT EXISTS user_token (
user_id INT NOT NULL ,
token TEXT NOT NULL ,
expires INT NOT NULL ,
PRIMARY KEY ( user_id , token ) ,
FOREIGN KEY ( user_id ) REFERENCES user ( id ) ON DELETE CASCADE
) ;
CREATE TABLE IF NOT EXISTS schemaVersion (
id INT PRIMARY KEY ,
version INT NOT NULL
) ;
INSERT INTO user ( id , user , pass , role ) VALUES ( 1 , '*' , ' ' , ' anonymous ' ) ON CONFLICT ( id ) DO NOTHING ;
`
2022-12-29 18:08:47 +00:00
createTablesQueries = ` BEGIN; ` + createTablesQueriesNoTx + ` COMMIT; `
2022-12-26 03:29:55 +00:00
selectUserByNameQuery = `
SELECT u . user , u . pass , u . role , u . messages , u . emails , u . settings , p . code , p . messages_limit , p . emails_limit , p . attachment_file_size_limit , p . attachment_total_size_limit
FROM user u
LEFT JOIN plan p on p . id = u . plan_id
WHERE user = ?
`
selectUserByTokenQuery = `
SELECT u . user , u . pass , u . role , u . messages , u . emails , u . settings , p . code , p . messages_limit , p . emails_limit , p . attachment_file_size_limit , p . attachment_total_size_limit
FROM user u
JOIN user_token t on u . id = t . user_id
LEFT JOIN plan p on p . id = u . plan_id
2022-12-29 16:09:45 +00:00
WHERE t . token = ? AND t . expires >= ?
2022-12-26 03:29:55 +00:00
`
selectTopicPermsQuery = `
2022-12-27 02:27:07 +00:00
SELECT read , write
FROM user_access a
JOIN user u ON u . id = a . user_id
WHERE ( u . user = '*' OR u . user = ? ) AND ? LIKE a . topic
ORDER BY u . user DESC
2022-12-26 03:29:55 +00:00
`
)
2022-01-26 03:30:53 +00:00
2022-12-26 03:29:55 +00:00
// Manager-related queries
const (
2022-12-28 18:28:28 +00:00
insertUserQuery = ` INSERT INTO user (user, pass, role) VALUES (?, ?, ?) `
selectUsernamesQuery = `
SELECT user
FROM user
ORDER BY
CASE role
WHEN ' admin ' THEN 1
WHEN ' anonymous ' THEN 3
ELSE 2
END , user
`
2022-12-26 03:29:55 +00:00
updateUserPassQuery = ` UPDATE user SET pass = ? WHERE user = ? `
updateUserRoleQuery = ` UPDATE user SET role = ? WHERE user = ? `
updateUserSettingsQuery = ` UPDATE user SET settings = ? WHERE user = ? `
updateUserStatsQuery = ` UPDATE user SET messages = ?, emails = ? WHERE user = ? `
deleteUserQuery = ` DELETE FROM user WHERE user = ? `
2022-12-28 18:28:28 +00:00
upsertUserAccessQuery = `
INSERT INTO user_access ( user_id , topic , read , write )
VALUES ( ( SELECT id FROM user WHERE user = ? ) , ? , ? , ? )
ON CONFLICT ( user_id , topic )
DO UPDATE SET read = excluded . read , write = excluded . write
`
selectUserAccessQuery = ` SELECT topic, read, write FROM user_access WHERE user_id = (SELECT id FROM user WHERE user = ?) ORDER BY write DESC, read DESC, topic `
2022-12-26 03:29:55 +00:00
deleteAllAccessQuery = ` DELETE FROM user_access `
deleteUserAccessQuery = ` DELETE FROM user_access WHERE user_id = (SELECT id FROM user WHERE user = ?) `
deleteTopicAccessQuery = ` DELETE FROM user_access WHERE user_id = (SELECT id FROM user WHERE user = ?) AND topic = ? `
insertTokenQuery = ` INSERT INTO user_token (user_id, token, expires) VALUES ((SELECT id FROM user WHERE user = ?), ?, ?) `
updateTokenExpiryQuery = ` UPDATE user_token SET expires = ? WHERE user_id = (SELECT id FROM user WHERE user = ?) AND token = ? `
deleteTokenQuery = ` DELETE FROM user_token WHERE user_id = (SELECT id FROM user WHERE user = ?) AND token = ? `
deleteExpiredTokensQuery = ` DELETE FROM user_token WHERE expires < ? `
deleteUserTokensQuery = ` DELETE FROM user_token WHERE user_id = (SELECT id FROM user WHERE user = ?) `
)
2022-12-03 20:20:59 +00:00
2022-12-26 03:29:55 +00:00
// Schema management queries
const (
2022-12-29 18:08:47 +00:00
currentSchemaVersion = 2
2022-12-26 03:29:55 +00:00
insertSchemaVersion = ` INSERT INTO schemaVersion VALUES (1, ?) `
2022-12-29 18:08:47 +00:00
updateSchemaVersion = ` UPDATE schemaVersion SET version = ? WHERE id = 1 `
2022-12-26 03:29:55 +00:00
selectSchemaVersionQuery = ` SELECT version FROM schemaVersion WHERE id = 1 `
2022-12-29 18:08:47 +00:00
// 1 -> 2 (complex migration!)
migrate1To2RenameUserTableQueryNoTx = `
ALTER TABLE user RENAME TO user_old ;
`
migrate1To2InsertFromOldTablesAndDropNoTx = `
INSERT INTO user ( user , pass , role )
SELECT user , pass , role FROM user_old ;
INSERT INTO user_access ( user_id , topic , read , write )
SELECT u . id , a . topic , a . read , a . write
FROM user u
JOIN access a ON u . user = a . user ;
DROP TABLE access ;
DROP TABLE user_old ;
`
2022-12-26 03:29:55 +00:00
)
2022-01-23 05:02:16 +00:00
2022-12-28 03:14:14 +00:00
// Manager is an implementation of Manager. It stores users and access control list
2022-12-26 03:29:55 +00:00
// in a SQLite database.
2022-12-28 03:14:14 +00:00
type Manager struct {
2022-12-29 16:09:45 +00:00
db * sql . DB
defaultRead bool // Default read permission if no ACL matches
defaultWrite bool // Default write permission if no ACL matches
statsQueue map [ string ] * User // Username -> User, for "unimportant" user updates
tokenExpiryInterval time . Duration // Duration after which tokens expire, and by which tokens are extended
mu sync . Mutex
2022-12-26 03:29:55 +00:00
}
2022-01-26 03:30:53 +00:00
2022-12-28 03:14:14 +00:00
var _ Auther = ( * Manager ) ( nil )
2022-12-26 03:29:55 +00:00
2022-12-28 03:14:14 +00:00
// NewManager creates a new Manager instance
func NewManager ( filename string , defaultRead , defaultWrite bool ) ( * Manager , error ) {
2022-12-29 16:09:45 +00:00
return newManager ( filename , defaultRead , defaultWrite , userTokenExpiryDuration , userStatsQueueWriterInterval )
}
// NewManager creates a new Manager instance
func newManager ( filename string , defaultRead , defaultWrite bool , tokenExpiryDuration , statsWriterInterval time . Duration ) ( * Manager , error ) {
2022-12-26 03:29:55 +00:00
db , err := sql . Open ( "sqlite3" , filename )
if err != nil {
return nil , err
}
2022-12-29 18:08:47 +00:00
if err := setupDB ( db ) ; err != nil {
2022-12-26 03:29:55 +00:00
return nil , err
}
2022-12-28 03:14:14 +00:00
manager := & Manager {
2022-12-29 16:09:45 +00:00
db : db ,
defaultRead : defaultRead ,
defaultWrite : defaultWrite ,
statsQueue : make ( map [ string ] * User ) ,
tokenExpiryInterval : tokenExpiryDuration ,
2022-12-26 03:29:55 +00:00
}
2022-12-29 16:09:45 +00:00
go manager . userStatsQueueWriter ( statsWriterInterval )
2022-12-26 03:29:55 +00:00
return manager , nil
}
2022-01-26 03:30:53 +00:00
2022-12-28 18:28:28 +00:00
// Authenticate checks username and password and returns a User if correct. The method
2022-12-26 03:29:55 +00:00
// returns in constant-ish time, regardless of whether the user exists or the password is
// correct or incorrect.
2022-12-28 03:14:14 +00:00
func ( a * Manager ) Authenticate ( username , password string ) ( * User , error ) {
2022-12-26 03:29:55 +00:00
if username == Everyone {
return nil , ErrUnauthenticated
}
user , err := a . User ( username )
if err != nil {
bcrypt . CompareHashAndPassword ( [ ] byte ( intentionalSlowDownHash ) ,
[ ] byte ( "intentional slow-down to avoid timing attacks" ) )
return nil , ErrUnauthenticated
}
if err := bcrypt . CompareHashAndPassword ( [ ] byte ( user . Hash ) , [ ] byte ( password ) ) ; err != nil {
return nil , ErrUnauthenticated
}
return user , nil
}
2022-01-26 03:30:53 +00:00
2022-12-28 18:28:28 +00:00
// AuthenticateToken checks if the token exists and returns the associated User if it does.
// The method sets the User.Token value to the token that was used for authentication.
2022-12-28 03:14:14 +00:00
func ( a * Manager ) AuthenticateToken ( token string ) ( * User , error ) {
2022-12-28 18:46:18 +00:00
if len ( token ) != tokenLength {
return nil , ErrUnauthenticated
}
2022-12-26 03:29:55 +00:00
user , err := a . userByToken ( token )
if err != nil {
return nil , ErrUnauthenticated
}
user . Token = token
return user , nil
}
2022-01-26 03:30:53 +00:00
2022-12-28 18:28:28 +00:00
// CreateToken generates a random token for the given user and returns it. The token expires
// after a fixed duration unless ExtendToken is called.
2022-12-28 03:14:14 +00:00
func ( a * Manager ) CreateToken ( user * User ) ( * Token , error ) {
2022-12-28 18:28:28 +00:00
token , expires := util . RandomString ( tokenLength ) , time . Now ( ) . Add ( userTokenExpiryDuration )
2022-12-26 03:29:55 +00:00
if _ , err := a . db . Exec ( insertTokenQuery , user . Name , token , expires . Unix ( ) ) ; err != nil {
return nil , err
}
return & Token {
Value : token ,
2022-12-28 18:46:18 +00:00
Expires : expires ,
2022-12-26 03:29:55 +00:00
} , nil
}
2022-01-26 03:30:53 +00:00
2022-12-28 18:28:28 +00:00
// ExtendToken sets the new expiry date for a token, thereby extending its use further into the future.
2022-12-28 03:14:14 +00:00
func ( a * Manager ) ExtendToken ( user * User ) ( * Token , error ) {
2022-12-26 03:29:55 +00:00
newExpires := time . Now ( ) . Add ( userTokenExpiryDuration )
if _ , err := a . db . Exec ( updateTokenExpiryQuery , newExpires . Unix ( ) , user . Name , user . Token ) ; err != nil {
return nil , err
}
return & Token {
Value : user . Token ,
2022-12-28 18:46:18 +00:00
Expires : newExpires ,
2022-12-26 03:29:55 +00:00
} , nil
}
2022-01-26 03:30:53 +00:00
2022-12-28 18:28:28 +00:00
// RemoveToken deletes the token defined in User.Token
2022-12-28 03:14:14 +00:00
func ( a * Manager ) RemoveToken ( user * User ) error {
2022-12-26 03:29:55 +00:00
if user . Token == "" {
return ErrUnauthorized
}
if _ , err := a . db . Exec ( deleteTokenQuery , user . Name , user . Token ) ; err != nil {
return err
}
return nil
}
2022-01-26 03:30:53 +00:00
2022-12-28 18:28:28 +00:00
// RemoveExpiredTokens deletes all expired tokens from the database
2022-12-28 03:14:14 +00:00
func ( a * Manager ) RemoveExpiredTokens ( ) error {
2022-12-26 03:29:55 +00:00
if _ , err := a . db . Exec ( deleteExpiredTokensQuery , time . Now ( ) . Unix ( ) ) ; err != nil {
return err
}
return nil
}
2022-01-26 03:30:53 +00:00
2022-12-28 20:51:09 +00:00
// ChangeSettings persists the user settings
2022-12-28 03:14:14 +00:00
func ( a * Manager ) ChangeSettings ( user * User ) error {
2022-12-26 03:29:55 +00:00
settings , err := json . Marshal ( user . Prefs )
if err != nil {
return err
}
if _ , err := a . db . Exec ( updateUserSettingsQuery , string ( settings ) , user . Name ) ; err != nil {
return err
}
return nil
2022-01-23 05:54:18 +00:00
}
2022-12-28 20:51:09 +00:00
// EnqueueStats adds the user to a queue which writes out user stats (messages, emails, ..) in
// batches at a regular interval
2022-12-28 03:14:14 +00:00
func ( a * Manager ) EnqueueStats ( user * User ) {
2022-12-26 03:29:55 +00:00
a . mu . Lock ( )
defer a . mu . Unlock ( )
a . statsQueue [ user . Name ] = user
2022-12-08 02:26:18 +00:00
}
2022-12-29 16:09:45 +00:00
func ( a * Manager ) userStatsQueueWriter ( interval time . Duration ) {
ticker := time . NewTicker ( interval )
2022-12-26 03:29:55 +00:00
for range ticker . C {
if err := a . writeUserStatsQueue ( ) ; err != nil {
log . Warn ( "UserManager: Writing user stats queue failed: %s" , err . Error ( ) )
}
}
2022-12-25 16:41:38 +00:00
}
2022-12-28 03:14:14 +00:00
func ( a * Manager ) writeUserStatsQueue ( ) error {
2022-12-26 03:29:55 +00:00
a . mu . Lock ( )
if len ( a . statsQueue ) == 0 {
a . mu . Unlock ( )
log . Trace ( "UserManager: No user stats updates to commit" )
return nil
}
statsQueue := a . statsQueue
a . statsQueue = make ( map [ string ] * User )
a . mu . Unlock ( )
tx , err := a . db . Begin ( )
if err != nil {
return err
}
defer tx . Rollback ( )
log . Debug ( "UserManager: Writing user stats queue for %d user(s)" , len ( statsQueue ) )
for username , u := range statsQueue {
log . Trace ( "UserManager: Updating stats for user %s: messages=%d, emails=%d" , username , u . Stats . Messages , u . Stats . Emails )
if _ , err := tx . Exec ( updateUserStatsQuery , u . Stats . Messages , u . Stats . Emails , username ) ; err != nil {
return err
}
}
return tx . Commit ( )
2022-12-08 02:26:18 +00:00
}
2022-12-26 03:29:55 +00:00
// Authorize returns nil if the given user has access to the given topic using the desired
// permission. The user param may be nil to signal an anonymous user.
2022-12-28 03:14:14 +00:00
func ( a * Manager ) Authorize ( user * User , topic string , perm Permission ) error {
2022-12-26 03:29:55 +00:00
if user != nil && user . Role == RoleAdmin {
return nil // Admin can do everything
}
username := Everyone
if user != nil {
username = user . Name
}
// Select the read/write permissions for this user/topic combo. The query may return two
// rows (one for everyone, and one for the user), but prioritizes the user. The value for
// user.Name may be empty (= everyone).
rows , err := a . db . Query ( selectTopicPermsQuery , username , topic )
if err != nil {
return err
}
defer rows . Close ( )
if ! rows . Next ( ) {
return a . resolvePerms ( a . defaultRead , a . defaultWrite , perm )
}
var read , write bool
if err := rows . Scan ( & read , & write ) ; err != nil {
return err
} else if err := rows . Err ( ) ; err != nil {
return err
}
return a . resolvePerms ( read , write , perm )
}
2022-12-18 19:35:05 +00:00
2022-12-28 03:14:14 +00:00
func ( a * Manager ) resolvePerms ( read , write bool , perm Permission ) error {
2022-12-26 03:29:55 +00:00
if perm == PermissionRead && read {
return nil
} else if perm == PermissionWrite && write {
return nil
}
return ErrUnauthorized
}
2022-12-18 19:35:05 +00:00
2022-12-29 00:55:11 +00:00
// AddUser adds a user with the given username, password and role
2022-12-28 03:14:14 +00:00
func ( a * Manager ) AddUser ( username , password string , role Role ) error {
2022-12-26 03:29:55 +00:00
if ! AllowedUsername ( username ) || ! AllowedRole ( role ) {
return ErrInvalidArgument
}
hash , err := bcrypt . GenerateFromPassword ( [ ] byte ( password ) , bcryptCost )
if err != nil {
return err
}
if _ , err = a . db . Exec ( insertUserQuery , username , hash , role ) ; err != nil {
return err
}
return nil
2022-12-17 20:17:52 +00:00
}
2022-12-26 03:29:55 +00:00
// RemoveUser deletes the user with the given username. The function returns nil on success, even
// if the user did not exist in the first place.
2022-12-28 03:14:14 +00:00
func ( a * Manager ) RemoveUser ( username string ) error {
2022-12-26 03:29:55 +00:00
if ! AllowedUsername ( username ) {
return ErrInvalidArgument
}
2022-12-29 18:08:47 +00:00
tx , err := a . db . Begin ( )
if err != nil {
2022-12-26 03:29:55 +00:00
return err
}
2022-12-29 18:08:47 +00:00
defer tx . Rollback ( )
if _ , err := tx . Exec ( deleteUserAccessQuery , username ) ; err != nil {
2022-12-26 03:29:55 +00:00
return err
}
2022-12-29 18:08:47 +00:00
if _ , err := tx . Exec ( deleteUserTokensQuery , username ) ; err != nil {
2022-12-26 03:29:55 +00:00
return err
}
2022-12-29 18:08:47 +00:00
if _ , err := tx . Exec ( deleteUserQuery , username ) ; err != nil {
return err
}
return tx . Commit ( )
2022-12-08 02:26:18 +00:00
}
2022-12-26 03:29:55 +00:00
// Users returns a list of users. It always also returns the Everyone user ("*").
2022-12-28 03:14:14 +00:00
func ( a * Manager ) Users ( ) ( [ ] * User , error ) {
2022-12-26 03:29:55 +00:00
rows , err := a . db . Query ( selectUsernamesQuery )
if err != nil {
return nil , err
}
defer rows . Close ( )
usernames := make ( [ ] string , 0 )
for rows . Next ( ) {
var username string
if err := rows . Scan ( & username ) ; err != nil {
return nil , err
} else if err := rows . Err ( ) ; err != nil {
return nil , err
}
usernames = append ( usernames , username )
}
rows . Close ( )
users := make ( [ ] * User , 0 )
for _ , username := range usernames {
user , err := a . User ( username )
if err != nil {
return nil , err
}
users = append ( users , user )
}
return users , nil
2022-01-24 04:02:39 +00:00
}
2022-12-26 03:29:55 +00:00
// User returns the user with the given username if it exists, or ErrNotFound otherwise.
// You may also pass Everyone to retrieve the anonymous user and its Grant list.
2022-12-28 03:14:14 +00:00
func ( a * Manager ) User ( username string ) ( * User , error ) {
2022-12-26 03:29:55 +00:00
rows , err := a . db . Query ( selectUserByNameQuery , username )
if err != nil {
return nil , err
}
return a . readUser ( rows )
2022-12-21 02:18:33 +00:00
}
2022-12-28 03:14:14 +00:00
func ( a * Manager ) userByToken ( token string ) ( * User , error ) {
2022-12-29 16:09:45 +00:00
rows , err := a . db . Query ( selectUserByTokenQuery , token , time . Now ( ) . Unix ( ) )
2022-12-26 03:29:55 +00:00
if err != nil {
return nil , err
}
return a . readUser ( rows )
2022-01-23 05:02:16 +00:00
}
2022-12-28 03:14:14 +00:00
func ( a * Manager ) readUser ( rows * sql . Rows ) ( * User , error ) {
2022-12-26 03:29:55 +00:00
defer rows . Close ( )
var username , hash , role string
var settings , planCode sql . NullString
var messages , emails int64
var messagesLimit , emailsLimit , attachmentFileSizeLimit , attachmentTotalSizeLimit sql . NullInt64
if ! rows . Next ( ) {
return nil , ErrNotFound
}
if err := rows . Scan ( & username , & hash , & role , & messages , & emails , & settings , & planCode , & messagesLimit , & emailsLimit , & attachmentFileSizeLimit , & attachmentTotalSizeLimit ) ; err != nil {
return nil , err
} else if err := rows . Err ( ) ; err != nil {
return nil , err
}
grants , err := a . readGrants ( username )
if err != nil {
return nil , err
}
user := & User {
Name : username ,
Hash : hash ,
Role : Role ( role ) ,
Grants : grants ,
Stats : & Stats {
Messages : messages ,
Emails : emails ,
} ,
}
if settings . Valid {
user . Prefs = & Prefs { }
if err := json . Unmarshal ( [ ] byte ( settings . String ) , user . Prefs ) ; err != nil {
return nil , err
}
}
if planCode . Valid {
user . Plan = & Plan {
Code : planCode . String ,
Upgradable : true , // FIXME
MessagesLimit : messagesLimit . Int64 ,
EmailsLimit : emailsLimit . Int64 ,
AttachmentFileSizeLimit : attachmentFileSizeLimit . Int64 ,
AttachmentTotalSizeLimit : attachmentTotalSizeLimit . Int64 ,
}
}
return user , nil
}
2022-01-23 05:02:16 +00:00
2022-12-28 03:14:14 +00:00
func ( a * Manager ) readGrants ( username string ) ( [ ] Grant , error ) {
2022-12-26 03:29:55 +00:00
rows , err := a . db . Query ( selectUserAccessQuery , username )
if err != nil {
return nil , err
}
defer rows . Close ( )
grants := make ( [ ] Grant , 0 )
for rows . Next ( ) {
var topic string
var read , write bool
if err := rows . Scan ( & topic , & read , & write ) ; err != nil {
return nil , err
} else if err := rows . Err ( ) ; err != nil {
return nil , err
}
grants = append ( grants , Grant {
TopicPattern : fromSQLWildcard ( topic ) ,
AllowRead : read ,
AllowWrite : write ,
} )
}
return grants , nil
}
2022-01-23 05:02:16 +00:00
2022-12-26 03:29:55 +00:00
// ChangePassword changes a user's password
2022-12-28 03:14:14 +00:00
func ( a * Manager ) ChangePassword ( username , password string ) error {
2022-12-26 03:29:55 +00:00
hash , err := bcrypt . GenerateFromPassword ( [ ] byte ( password ) , bcryptCost )
if err != nil {
return err
}
if _ , err := a . db . Exec ( updateUserPassQuery , hash , username ) ; err != nil {
return err
}
return nil
}
2022-01-23 05:02:16 +00:00
2022-12-26 03:29:55 +00:00
// ChangeRole changes a user's role. When a role is changed from RoleUser to RoleAdmin,
// all existing access control entries (Grant) are removed, since they are no longer needed.
2022-12-28 03:14:14 +00:00
func ( a * Manager ) ChangeRole ( username string , role Role ) error {
2022-12-26 03:29:55 +00:00
if ! AllowedUsername ( username ) || ! AllowedRole ( role ) {
return ErrInvalidArgument
}
if _ , err := a . db . Exec ( updateUserRoleQuery , string ( role ) , username ) ; err != nil {
return err
}
if role == RoleAdmin {
if _ , err := a . db . Exec ( deleteUserAccessQuery , username ) ; err != nil {
return err
}
}
return nil
}
2022-01-23 20:30:30 +00:00
2022-12-26 03:29:55 +00:00
// AllowAccess adds or updates an entry in th access control list for a specific user. It controls
// read/write access to a topic. The parameter topicPattern may include wildcards (*).
2022-12-28 03:14:14 +00:00
func ( a * Manager ) AllowAccess ( username string , topicPattern string , read bool , write bool ) error {
2022-12-26 03:29:55 +00:00
if ( ! AllowedUsername ( username ) && username != Everyone ) || ! AllowedTopicPattern ( topicPattern ) {
return ErrInvalidArgument
}
if _ , err := a . db . Exec ( upsertUserAccessQuery , username , toSQLWildcard ( topicPattern ) , read , write ) ; err != nil {
return err
}
return nil
}
2022-01-31 16:44:58 +00:00
2022-12-26 03:29:55 +00:00
// ResetAccess removes an access control list entry for a specific username/topic, or (if topic is
// empty) for an entire user. The parameter topicPattern may include wildcards (*).
2022-12-28 03:14:14 +00:00
func ( a * Manager ) ResetAccess ( username string , topicPattern string ) error {
2022-12-26 03:29:55 +00:00
if ! AllowedUsername ( username ) && username != Everyone && username != "" {
return ErrInvalidArgument
} else if ! AllowedTopicPattern ( topicPattern ) && topicPattern != "" {
return ErrInvalidArgument
}
if username == "" && topicPattern == "" {
_ , err := a . db . Exec ( deleteAllAccessQuery , username )
return err
} else if topicPattern == "" {
_ , err := a . db . Exec ( deleteUserAccessQuery , username )
return err
}
_ , err := a . db . Exec ( deleteTopicAccessQuery , username , toSQLWildcard ( topicPattern ) )
return err
2022-01-23 20:30:30 +00:00
}
2022-12-26 03:29:55 +00:00
// DefaultAccess returns the default read/write access if no access control entry matches
2022-12-28 03:14:14 +00:00
func ( a * Manager ) DefaultAccess ( ) ( read bool , write bool ) {
2022-12-26 03:29:55 +00:00
return a . defaultRead , a . defaultWrite
2022-01-31 16:44:58 +00:00
}
2022-12-26 03:29:55 +00:00
func toSQLWildcard ( s string ) string {
return strings . ReplaceAll ( s , "*" , "%" )
2022-01-31 16:44:58 +00:00
}
2022-12-26 03:29:55 +00:00
func fromSQLWildcard ( s string ) string {
return strings . ReplaceAll ( s , "%" , "*" )
}
2022-12-29 18:08:47 +00:00
func setupDB ( db * sql . DB ) error {
2022-12-26 03:29:55 +00:00
// If 'schemaVersion' table does not exist, this must be a new database
rowsSV , err := db . Query ( selectSchemaVersionQuery )
if err != nil {
2022-12-29 18:08:47 +00:00
return setupNewDB ( db )
2022-12-26 03:29:55 +00:00
}
defer rowsSV . Close ( )
// If 'schemaVersion' table exists, read version and potentially upgrade
schemaVersion := 0
if ! rowsSV . Next ( ) {
return errors . New ( "cannot determine schema version: database file may be corrupt" )
}
if err := rowsSV . Scan ( & schemaVersion ) ; err != nil {
return err
}
rowsSV . Close ( )
// Do migrations
if schemaVersion == currentSchemaVersion {
return nil
2022-12-29 18:08:47 +00:00
} else if schemaVersion == 1 {
return migrateFrom1 ( db )
2022-12-26 03:29:55 +00:00
}
return fmt . Errorf ( "unexpected schema version found: %d" , schemaVersion )
}
2022-12-29 18:08:47 +00:00
func setupNewDB ( db * sql . DB ) error {
if _ , err := db . Exec ( createTablesQueries ) ; err != nil {
2022-12-26 03:29:55 +00:00
return err
}
if _ , err := db . Exec ( insertSchemaVersion , currentSchemaVersion ) ; err != nil {
return err
}
return nil
}
2022-12-29 18:08:47 +00:00
func migrateFrom1 ( db * sql . DB ) error {
log . Info ( "Migrating user database schema: from 1 to 2" )
tx , err := db . Begin ( )
if err != nil {
return err
}
defer tx . Rollback ( )
if _ , err := tx . Exec ( migrate1To2RenameUserTableQueryNoTx ) ; err != nil {
return err
}
if _ , err := tx . Exec ( createTablesQueriesNoTx ) ; err != nil {
return err
}
if _ , err := tx . Exec ( migrate1To2InsertFromOldTablesAndDropNoTx ) ; err != nil {
return err
}
if _ , err := tx . Exec ( updateSchemaVersion , 2 ) ; err != nil {
return err
}
if err := tx . Commit ( ) ; err != nil {
return err
}
return nil // Update this when a new version is added
}