mirror of
https://github.com/binwiederhier/ntfy.git
synced 2025-07-20 10:04:08 +00:00
User-owned ACL entries
This commit is contained in:
parent
598d0bdda3
commit
2267d27c9b
9 changed files with 160 additions and 57 deletions
|
@ -23,7 +23,8 @@ const (
|
|||
)
|
||||
|
||||
var (
|
||||
errNoTokenProvided = errors.New("no token provided")
|
||||
errNoTokenProvided = errors.New("no token provided")
|
||||
errTopicOwnedByOthers = errors.New("topic owned by others")
|
||||
)
|
||||
|
||||
// Manager-related queries
|
||||
|
@ -52,13 +53,13 @@ const (
|
|||
CREATE UNIQUE INDEX idx_user ON user (user);
|
||||
CREATE TABLE IF NOT EXISTS user_access (
|
||||
user_id INT NOT NULL,
|
||||
owner_user_id INT,
|
||||
topic TEXT NOT NULL,
|
||||
read INT NOT NULL,
|
||||
write INT NOT NULL,
|
||||
owner_user_id INT,
|
||||
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,
|
||||
|
@ -115,12 +116,23 @@ const (
|
|||
deleteUserQuery = `DELETE FROM user WHERE user = ?`
|
||||
|
||||
upsertUserAccessQuery = `
|
||||
INSERT INTO user_access (user_id, topic, read, write)
|
||||
VALUES ((SELECT id FROM user WHERE user = ?), ?, ?, ?)
|
||||
INSERT INTO user_access (user_id, topic, read, write, owner_user_id)
|
||||
VALUES ((SELECT id FROM user WHERE user = ?), ?, ?, ?, (SELECT IIF(?='',NULL,(SELECT id FROM user WHERE user=?))))
|
||||
ON CONFLICT (user_id, topic)
|
||||
DO UPDATE SET read=excluded.read, write=excluded.write
|
||||
DO UPDATE SET read=excluded.read, write=excluded.write, owner_user_id=excluded.owner_user_id
|
||||
`
|
||||
selectUserAccessQuery = `
|
||||
SELECT topic, read, write, IIF(owner_user_id IS NOT NULL AND user_id = owner_user_id,1,0) AS owner
|
||||
FROM user_access
|
||||
WHERE user_id = (SELECT id FROM user WHERE user = ?)
|
||||
ORDER BY write DESC, read DESC, topic
|
||||
`
|
||||
selectOtherAccessCountQuery = `
|
||||
SELECT count(*)
|
||||
FROM user_access
|
||||
WHERE (topic = ? OR ? LIKE topic)
|
||||
AND (owner_user_id IS NULL OR owner_user_id != (SELECT id FROM user WHERE user = ?))
|
||||
`
|
||||
selectUserAccessQuery = `SELECT topic, read, write FROM user_access WHERE user_id = (SELECT id FROM user WHERE user = ?) ORDER BY write DESC, read DESC, topic`
|
||||
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 = ?`
|
||||
|
@ -340,8 +352,7 @@ func (a *Manager) Authorize(user *User, topic string, perm Permission) error {
|
|||
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 (one for everyone, and one for the user), but prioritizes the user.
|
||||
rows, err := a.db.Query(selectTopicPermsQuery, username, topic)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -509,8 +520,8 @@ func (a *Manager) readGrants(username string) ([]Grant, error) {
|
|||
grants := make([]Grant, 0)
|
||||
for rows.Next() {
|
||||
var topic string
|
||||
var read, write bool
|
||||
if err := rows.Scan(&topic, &read, &write); err != nil {
|
||||
var read, write, owner bool
|
||||
if err := rows.Scan(&topic, &read, &write, &owner); err != nil {
|
||||
return nil, err
|
||||
} else if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
|
@ -519,6 +530,7 @@ func (a *Manager) readGrants(username string) ([]Grant, error) {
|
|||
TopicPattern: fromSQLWildcard(topic),
|
||||
AllowRead: read,
|
||||
AllowWrite: write,
|
||||
Owner: owner,
|
||||
})
|
||||
}
|
||||
return grants, nil
|
||||
|
@ -553,13 +565,42 @@ func (a *Manager) ChangeRole(username string, role Role) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// 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 (*).
|
||||
func (a *Manager) AllowAccess(username string, topicPattern string, read bool, write bool) error {
|
||||
if (!AllowedUsername(username) && username != Everyone) || !AllowedTopicPattern(topicPattern) {
|
||||
// CheckAllowAccess tests if a user may create an access control entry for the given topic.
|
||||
// If there are any ACL entries that are not owned by the user, an error is returned.
|
||||
func (a *Manager) CheckAllowAccess(username string, topic string) error {
|
||||
if (!AllowedUsername(username) && username != Everyone) || !AllowedTopic(topic) {
|
||||
return ErrInvalidArgument
|
||||
}
|
||||
if _, err := a.db.Exec(upsertUserAccessQuery, username, toSQLWildcard(topicPattern), read, write); err != nil {
|
||||
rows, err := a.db.Query(selectOtherAccessCountQuery, topic, topic, username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
if !rows.Next() {
|
||||
return errors.New("no rows found")
|
||||
}
|
||||
var otherCount int
|
||||
if err := rows.Scan(&otherCount); err != nil {
|
||||
return err
|
||||
}
|
||||
if otherCount > 0 {
|
||||
return errTopicOwnedByOthers
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 (*). The ACL entry
|
||||
// owner may either be a user (username), or the system (empty).
|
||||
func (a *Manager) AllowAccess(owner, username string, topicPattern string, read bool, write bool) error {
|
||||
if !AllowedUsername(username) && username != Everyone {
|
||||
return ErrInvalidArgument
|
||||
} else if owner != "" && !AllowedUsername(owner) {
|
||||
return ErrInvalidArgument
|
||||
} else if !AllowedTopicPattern(topicPattern) {
|
||||
return ErrInvalidArgument
|
||||
}
|
||||
if _, err := a.db.Exec(upsertUserAccessQuery, username, toSQLWildcard(topicPattern), read, write, owner, owner); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -15,13 +15,13 @@ func TestManager_FullScenario_Default_DenyAll(t *testing.T) {
|
|||
a := newTestManager(t, false, false)
|
||||
require.Nil(t, a.AddUser("phil", "phil", RoleAdmin))
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
|
||||
require.Nil(t, a.AllowAccess("ben", "mytopic", true, true))
|
||||
require.Nil(t, a.AllowAccess("ben", "readme", true, false))
|
||||
require.Nil(t, a.AllowAccess("ben", "writeme", false, true))
|
||||
require.Nil(t, a.AllowAccess("ben", "everyonewrite", false, false)) // How unfair!
|
||||
require.Nil(t, a.AllowAccess(Everyone, "announcements", true, false))
|
||||
require.Nil(t, a.AllowAccess(Everyone, "everyonewrite", true, true))
|
||||
require.Nil(t, a.AllowAccess(Everyone, "up*", false, true)) // Everyone can write to /up*
|
||||
require.Nil(t, a.AllowAccess("", "ben", "mytopic", true, true))
|
||||
require.Nil(t, a.AllowAccess("", "ben", "readme", true, false))
|
||||
require.Nil(t, a.AllowAccess("", "ben", "writeme", false, true))
|
||||
require.Nil(t, a.AllowAccess("", "ben", "everyonewrite", false, false)) // How unfair!
|
||||
require.Nil(t, a.AllowAccess("", Everyone, "announcements", true, false))
|
||||
require.Nil(t, a.AllowAccess("", Everyone, "everyonewrite", true, true))
|
||||
require.Nil(t, a.AllowAccess("", Everyone, "up*", false, true)) // Everyone can write to /up*
|
||||
|
||||
phil, err := a.Authenticate("phil", "phil")
|
||||
require.Nil(t, err)
|
||||
|
@ -36,10 +36,10 @@ func TestManager_FullScenario_Default_DenyAll(t *testing.T) {
|
|||
require.True(t, strings.HasPrefix(ben.Hash, "$2a$10$"))
|
||||
require.Equal(t, RoleUser, ben.Role)
|
||||
require.Equal(t, []Grant{
|
||||
{"mytopic", true, true},
|
||||
{"writeme", false, true},
|
||||
{"readme", true, false},
|
||||
{"everyonewrite", false, false},
|
||||
{"mytopic", true, true, false},
|
||||
{"writeme", false, true, false},
|
||||
{"readme", true, false, false},
|
||||
{"everyonewrite", false, false, false},
|
||||
}, ben.Grants)
|
||||
|
||||
notben, err := a.Authenticate("ben", "this is wrong")
|
||||
|
@ -124,12 +124,12 @@ func TestManager_UserManagement(t *testing.T) {
|
|||
a := newTestManager(t, false, false)
|
||||
require.Nil(t, a.AddUser("phil", "phil", RoleAdmin))
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
|
||||
require.Nil(t, a.AllowAccess("ben", "mytopic", true, true))
|
||||
require.Nil(t, a.AllowAccess("ben", "readme", true, false))
|
||||
require.Nil(t, a.AllowAccess("ben", "writeme", false, true))
|
||||
require.Nil(t, a.AllowAccess("ben", "everyonewrite", false, false)) // How unfair!
|
||||
require.Nil(t, a.AllowAccess(Everyone, "announcements", true, false))
|
||||
require.Nil(t, a.AllowAccess(Everyone, "everyonewrite", true, true))
|
||||
require.Nil(t, a.AllowAccess("", "ben", "mytopic", true, true))
|
||||
require.Nil(t, a.AllowAccess("", "ben", "readme", true, false))
|
||||
require.Nil(t, a.AllowAccess("", "ben", "writeme", false, true))
|
||||
require.Nil(t, a.AllowAccess("", "ben", "everyonewrite", false, false)) // How unfair!
|
||||
require.Nil(t, a.AllowAccess("", Everyone, "announcements", true, false))
|
||||
require.Nil(t, a.AllowAccess("", Everyone, "everyonewrite", true, true))
|
||||
|
||||
// Query user details
|
||||
phil, err := a.User("phil")
|
||||
|
@ -145,10 +145,10 @@ func TestManager_UserManagement(t *testing.T) {
|
|||
require.True(t, strings.HasPrefix(ben.Hash, "$2a$10$"))
|
||||
require.Equal(t, RoleUser, ben.Role)
|
||||
require.Equal(t, []Grant{
|
||||
{"mytopic", true, true},
|
||||
{"writeme", false, true},
|
||||
{"readme", true, false},
|
||||
{"everyonewrite", false, false},
|
||||
{"mytopic", true, true, false},
|
||||
{"writeme", false, true, false},
|
||||
{"readme", true, false, false},
|
||||
{"everyonewrite", false, false, false},
|
||||
}, ben.Grants)
|
||||
|
||||
everyone, err := a.User(Everyone)
|
||||
|
@ -157,14 +157,14 @@ func TestManager_UserManagement(t *testing.T) {
|
|||
require.Equal(t, "", everyone.Hash)
|
||||
require.Equal(t, RoleAnonymous, everyone.Role)
|
||||
require.Equal(t, []Grant{
|
||||
{"everyonewrite", true, true},
|
||||
{"announcements", true, false},
|
||||
{"everyonewrite", true, true, false},
|
||||
{"announcements", true, false, false},
|
||||
}, everyone.Grants)
|
||||
|
||||
// Ben: Before revoking
|
||||
require.Nil(t, a.AllowAccess("ben", "mytopic", true, true)) // Overwrite!
|
||||
require.Nil(t, a.AllowAccess("ben", "readme", true, false))
|
||||
require.Nil(t, a.AllowAccess("ben", "writeme", false, true))
|
||||
require.Nil(t, a.AllowAccess("", "ben", "mytopic", true, true)) // Overwrite!
|
||||
require.Nil(t, a.AllowAccess("", "ben", "readme", true, false))
|
||||
require.Nil(t, a.AllowAccess("", "ben", "writeme", false, true))
|
||||
require.Nil(t, a.Authorize(ben, "mytopic", PermissionRead))
|
||||
require.Nil(t, a.Authorize(ben, "mytopic", PermissionWrite))
|
||||
require.Nil(t, a.Authorize(ben, "readme", PermissionRead))
|
||||
|
@ -219,8 +219,8 @@ func TestManager_ChangePassword(t *testing.T) {
|
|||
func TestManager_ChangeRole(t *testing.T) {
|
||||
a := newTestManager(t, false, false)
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
|
||||
require.Nil(t, a.AllowAccess("ben", "mytopic", true, true))
|
||||
require.Nil(t, a.AllowAccess("ben", "readme", true, false))
|
||||
require.Nil(t, a.AllowAccess("", "ben", "mytopic", true, true))
|
||||
require.Nil(t, a.AllowAccess("", "ben", "readme", true, false))
|
||||
|
||||
ben, err := a.User("ben")
|
||||
require.Nil(t, err)
|
||||
|
|
|
@ -90,6 +90,7 @@ type Grant struct {
|
|||
TopicPattern string // May include wildcard (*)
|
||||
AllowRead bool
|
||||
AllowWrite bool
|
||||
Owner bool // This user owns this ACL entry
|
||||
}
|
||||
|
||||
// Permission represents a read or write permission to a topic
|
||||
|
@ -118,6 +119,7 @@ const (
|
|||
|
||||
var (
|
||||
allowedUsernameRegex = regexp.MustCompile(`^[-_.@a-zA-Z0-9]+$`) // Does not include Everyone (*)
|
||||
allowedTopicRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`) // No '*'
|
||||
allowedTopicPatternRegex = regexp.MustCompile(`^[-_*A-Za-z0-9]{1,64}$`) // Adds '*' for wildcards!
|
||||
)
|
||||
|
||||
|
@ -131,6 +133,11 @@ func AllowedUsername(username string) bool {
|
|||
return allowedUsernameRegex.MatchString(username)
|
||||
}
|
||||
|
||||
// AllowedTopic returns true if the given topic name is valid
|
||||
func AllowedTopic(username string) bool {
|
||||
return allowedTopicRegex.MatchString(username)
|
||||
}
|
||||
|
||||
// AllowedTopicPattern returns true if the given topic pattern is valid; this includes the wildcard character (*)
|
||||
func AllowedTopicPattern(username string) bool {
|
||||
return allowedTopicPatternRegex.MatchString(username)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue