mirror of
https://github.com/binwiederhier/ntfy.git
synced 2025-07-20 10:04:08 +00:00
Tiers make sense for admins now
This commit is contained in:
parent
d8032e1c9e
commit
3aba7404fc
18 changed files with 457 additions and 225 deletions
|
@ -35,6 +35,8 @@ const (
|
|||
CREATE TABLE IF NOT EXISTS tier (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
code TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
paid INT NOT NULL,
|
||||
messages_limit INT NOT NULL,
|
||||
messages_expiry_duration INT NOT NULL,
|
||||
emails_limit INT NOT NULL,
|
||||
|
@ -84,13 +86,13 @@ const (
|
|||
`
|
||||
|
||||
selectUserByNameQuery = `
|
||||
SELECT u.user, u.pass, u.role, u.messages, u.emails, u.settings, p.code, p.messages_limit, p.messages_expiry_duration, p.emails_limit, p.reservations_limit, p.attachment_file_size_limit, p.attachment_total_size_limit, p.attachment_expiry_duration
|
||||
SELECT u.user, u.pass, u.role, u.messages, u.emails, u.settings, p.code, p.name, p.paid, p.messages_limit, p.messages_expiry_duration, p.emails_limit, p.reservations_limit, p.attachment_file_size_limit, p.attachment_total_size_limit, p.attachment_expiry_duration
|
||||
FROM user u
|
||||
LEFT JOIN tier p on p.id = u.tier_id
|
||||
WHERE user = ?
|
||||
`
|
||||
selectUserByTokenQuery = `
|
||||
SELECT u.user, u.pass, u.role, u.messages, u.emails, u.settings, p.code, p.messages_limit, p.messages_expiry_duration, p.emails_limit, p.reservations_limit, p.attachment_file_size_limit, p.attachment_total_size_limit, p.attachment_expiry_duration
|
||||
SELECT u.user, u.pass, u.role, u.messages, u.emails, u.settings, p.code, p.name, p.paid, p.messages_limit, p.messages_expiry_duration, p.emails_limit, p.reservations_limit, p.attachment_file_size_limit, p.attachment_total_size_limit, p.attachment_expiry_duration
|
||||
FROM user u
|
||||
JOIN user_token t on u.id = t.user_id
|
||||
LEFT JOIN tier p on p.id = u.tier_id
|
||||
|
@ -159,9 +161,17 @@ const (
|
|||
WHERE (topic = ? OR ? LIKE topic)
|
||||
AND (owner_user_id IS NULL OR owner_user_id != (SELECT id FROM user WHERE user = ?))
|
||||
`
|
||||
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 = ?`
|
||||
deleteAllAccessQuery = `DELETE FROM user_access`
|
||||
deleteUserAccessQuery = `
|
||||
DELETE FROM user_access
|
||||
WHERE user_id = (SELECT id FROM user WHERE user = ?)
|
||||
OR owner_user_id = (SELECT id FROM user WHERE user = ?)
|
||||
`
|
||||
deleteTopicAccessQuery = `
|
||||
DELETE FROM user_access
|
||||
WHERE (user_id = (SELECT id FROM user WHERE user = ?) OR owner_user_id = (SELECT id FROM user WHERE user = ?))
|
||||
AND topic = ?
|
||||
`
|
||||
|
||||
selectTokenCountQuery = `SELECT COUNT(*) FROM user_token WHERE (SELECT id FROM user WHERE user = ?)`
|
||||
insertTokenQuery = `INSERT INTO user_token (user_id, token, expires) VALUES ((SELECT id FROM user WHERE user = ?), ?, ?)`
|
||||
|
@ -180,11 +190,12 @@ const (
|
|||
`
|
||||
|
||||
insertTierQuery = `
|
||||
INSERT INTO tier (code, messages_limit, messages_expiry_duration, emails_limit, reservations_limit, attachment_file_size_limit, attachment_total_size_limit, attachment_expiry_duration)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO tier (code, name, paid, messages_limit, messages_expiry_duration, emails_limit, reservations_limit, attachment_file_size_limit, attachment_total_size_limit, attachment_expiry_duration)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
selectTierIDQuery = `SELECT id FROM tier WHERE code = ?`
|
||||
updateUserTierQuery = `UPDATE user SET tier_id = ? WHERE user = ?`
|
||||
deleteUserTierQuery = `UPDATE user SET tier_id = null WHERE user = ?`
|
||||
)
|
||||
|
||||
// Schema management queries
|
||||
|
@ -528,13 +539,14 @@ func (a *Manager) userByToken(token string) (*User, error) {
|
|||
func (a *Manager) readUser(rows *sql.Rows) (*User, error) {
|
||||
defer rows.Close()
|
||||
var username, hash, role string
|
||||
var settings, tierCode sql.NullString
|
||||
var settings, tierCode, tierName sql.NullString
|
||||
var paid sql.NullBool
|
||||
var messages, emails int64
|
||||
var messagesLimit, messagesExpiryDuration, emailsLimit, reservationsLimit, attachmentFileSizeLimit, attachmentTotalSizeLimit, attachmentExpiryDuration sql.NullInt64
|
||||
if !rows.Next() {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
if err := rows.Scan(&username, &hash, &role, &messages, &emails, &settings, &tierCode, &messagesLimit, &messagesExpiryDuration, &emailsLimit, &reservationsLimit, &attachmentFileSizeLimit, &attachmentTotalSizeLimit, &attachmentExpiryDuration); err != nil {
|
||||
if err := rows.Scan(&username, &hash, &role, &messages, &emails, &settings, &tierCode, &tierName, &paid, &messagesLimit, &messagesExpiryDuration, &emailsLimit, &reservationsLimit, &attachmentFileSizeLimit, &attachmentTotalSizeLimit, &attachmentExpiryDuration); err != nil {
|
||||
return nil, err
|
||||
} else if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
|
@ -557,14 +569,15 @@ func (a *Manager) readUser(rows *sql.Rows) (*User, error) {
|
|||
if tierCode.Valid {
|
||||
user.Tier = &Tier{
|
||||
Code: tierCode.String,
|
||||
Upgradeable: false,
|
||||
Name: tierName.String,
|
||||
Paid: paid.Bool,
|
||||
MessagesLimit: messagesLimit.Int64,
|
||||
MessagesExpiryDuration: messagesExpiryDuration.Int64,
|
||||
MessagesExpiryDuration: time.Duration(messagesExpiryDuration.Int64) * time.Second,
|
||||
EmailsLimit: emailsLimit.Int64,
|
||||
ReservationsLimit: reservationsLimit.Int64,
|
||||
AttachmentFileSizeLimit: attachmentFileSizeLimit.Int64,
|
||||
AttachmentTotalSizeLimit: attachmentTotalSizeLimit.Int64,
|
||||
AttachmentExpiryDuration: attachmentExpiryDuration.Int64,
|
||||
AttachmentExpiryDuration: time.Duration(attachmentExpiryDuration.Int64) * time.Second,
|
||||
}
|
||||
}
|
||||
return user, nil
|
||||
|
@ -676,7 +689,7 @@ func (a *Manager) ChangeRole(username string, role Role) error {
|
|||
return err
|
||||
}
|
||||
if role == RoleAdmin {
|
||||
if _, err := a.db.Exec(deleteUserAccessQuery, username); err != nil {
|
||||
if _, err := a.db.Exec(deleteUserAccessQuery, username, username); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -760,10 +773,19 @@ func (a *Manager) ResetAccess(username string, topicPattern string) error {
|
|||
_, err := a.db.Exec(deleteAllAccessQuery, username)
|
||||
return err
|
||||
} else if topicPattern == "" {
|
||||
_, err := a.db.Exec(deleteUserAccessQuery, username)
|
||||
_, err := a.db.Exec(deleteUserAccessQuery, username, username)
|
||||
return err
|
||||
}
|
||||
_, err := a.db.Exec(deleteTopicAccessQuery, username, toSQLWildcard(topicPattern))
|
||||
_, err := a.db.Exec(deleteTopicAccessQuery, username, username, toSQLWildcard(topicPattern))
|
||||
return err
|
||||
}
|
||||
|
||||
// ResetTier removes the tier from the given user
|
||||
func (a *Manager) ResetTier(username string) error {
|
||||
if !AllowedUsername(username) && username != Everyone && username != "" {
|
||||
return ErrInvalidArgument
|
||||
}
|
||||
_, err := a.db.Exec(deleteUserTierQuery, username)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -774,7 +796,7 @@ func (a *Manager) DefaultAccess() Permission {
|
|||
|
||||
// CreateTier creates a new tier in the database
|
||||
func (a *Manager) CreateTier(tier *Tier) error {
|
||||
if _, err := a.db.Exec(insertTierQuery, tier.Code, tier.MessagesLimit, tier.MessagesExpiryDuration, tier.EmailsLimit, tier.ReservationsLimit, tier.AttachmentFileSizeLimit, tier.AttachmentTotalSizeLimit, tier.AttachmentExpiryDuration); err != nil {
|
||||
if _, err := a.db.Exec(insertTierQuery, tier.Code, tier.Name, tier.Paid, tier.MessagesLimit, int64(tier.MessagesExpiryDuration.Seconds()), tier.EmailsLimit, tier.ReservationsLimit, tier.AttachmentFileSizeLimit, tier.AttachmentTotalSizeLimit, int64(tier.AttachmentExpiryDuration.Seconds())); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -256,6 +256,60 @@ func TestManager_ChangeRole(t *testing.T) {
|
|||
require.Equal(t, 0, len(benGrants))
|
||||
}
|
||||
|
||||
func TestManager_ChangeRoleFromTierUserToAdmin(t *testing.T) {
|
||||
a := newTestManager(t, PermissionDenyAll)
|
||||
require.Nil(t, a.CreateTier(&Tier{
|
||||
Code: "pro",
|
||||
Name: "ntfy Pro",
|
||||
Paid: true,
|
||||
MessagesLimit: 5_000,
|
||||
MessagesExpiryDuration: 3 * 24 * time.Hour,
|
||||
EmailsLimit: 50,
|
||||
ReservationsLimit: 5,
|
||||
AttachmentFileSizeLimit: 52428800,
|
||||
AttachmentTotalSizeLimit: 524288000,
|
||||
AttachmentExpiryDuration: 24 * time.Hour,
|
||||
}))
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
|
||||
require.Nil(t, a.ChangeTier("ben", "pro"))
|
||||
require.Nil(t, a.AllowAccess("ben", "ben", "mytopic", true, true))
|
||||
require.Nil(t, a.AllowAccess("ben", Everyone, "mytopic", false, false))
|
||||
|
||||
ben, err := a.User("ben")
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, RoleUser, ben.Role)
|
||||
require.Equal(t, "pro", ben.Tier.Code)
|
||||
require.Equal(t, true, ben.Tier.Paid)
|
||||
require.Equal(t, int64(5000), ben.Tier.MessagesLimit)
|
||||
require.Equal(t, 3*24*time.Hour, ben.Tier.MessagesExpiryDuration)
|
||||
require.Equal(t, int64(50), ben.Tier.EmailsLimit)
|
||||
require.Equal(t, int64(5), ben.Tier.ReservationsLimit)
|
||||
require.Equal(t, int64(52428800), ben.Tier.AttachmentFileSizeLimit)
|
||||
require.Equal(t, int64(524288000), ben.Tier.AttachmentTotalSizeLimit)
|
||||
require.Equal(t, 24*time.Hour, ben.Tier.AttachmentExpiryDuration)
|
||||
|
||||
benGrants, err := a.Grants("ben")
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 1, len(benGrants))
|
||||
require.Equal(t, PermissionReadWrite, benGrants[0].Allow)
|
||||
|
||||
everyoneGrants, err := a.Grants(Everyone)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 1, len(everyoneGrants))
|
||||
require.Equal(t, PermissionDenyAll, everyoneGrants[0].Allow)
|
||||
|
||||
// Switch to admin, this should remove all grants and owned ACL entries
|
||||
require.Nil(t, a.ChangeRole("ben", RoleAdmin))
|
||||
|
||||
benGrants, err = a.Grants("ben")
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 0, len(benGrants))
|
||||
|
||||
everyoneGrants, err = a.Grants(Everyone)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 0, len(everyoneGrants))
|
||||
}
|
||||
|
||||
func TestManager_Token_Valid(t *testing.T) {
|
||||
a := newTestManager(t, PermissionDenyAll)
|
||||
require.Nil(t, a.AddUser("ben", "ben", RoleUser))
|
||||
|
|
|
@ -43,27 +43,18 @@ type Prefs struct {
|
|||
Subscriptions []*Subscription `json:"subscriptions,omitempty"`
|
||||
}
|
||||
|
||||
// TierCode is code identifying a user's tier
|
||||
type TierCode string
|
||||
|
||||
// Default tier codes
|
||||
const (
|
||||
TierUnlimited = TierCode("unlimited")
|
||||
TierDefault = TierCode("default")
|
||||
TierNone = TierCode("none")
|
||||
)
|
||||
|
||||
// Tier represents a user's account type, including its account limits
|
||||
type Tier struct {
|
||||
Code string `json:"name"`
|
||||
Upgradeable bool `json:"upgradeable"`
|
||||
MessagesLimit int64 `json:"messages_limit"`
|
||||
MessagesExpiryDuration int64 `json:"messages_expiry_duration"`
|
||||
EmailsLimit int64 `json:"emails_limit"`
|
||||
ReservationsLimit int64 `json:"reservations_limit"`
|
||||
AttachmentFileSizeLimit int64 `json:"attachment_file_size_limit"`
|
||||
AttachmentTotalSizeLimit int64 `json:"attachment_total_size_limit"`
|
||||
AttachmentExpiryDuration int64 `json:"attachment_expiry_duration"`
|
||||
Code string
|
||||
Name string
|
||||
Paid bool
|
||||
MessagesLimit int64
|
||||
MessagesExpiryDuration time.Duration
|
||||
EmailsLimit int64
|
||||
ReservationsLimit int64
|
||||
AttachmentFileSizeLimit int64
|
||||
AttachmentTotalSizeLimit int64
|
||||
AttachmentExpiryDuration time.Duration
|
||||
}
|
||||
|
||||
// Subscription represents a user's topic subscription
|
||||
|
@ -185,6 +176,7 @@ 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!
|
||||
allowedTierRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`)
|
||||
)
|
||||
|
||||
// AllowedRole returns true if the given role can be used for new users
|
||||
|
@ -198,13 +190,18 @@ func AllowedUsername(username string) bool {
|
|||
}
|
||||
|
||||
// AllowedTopic returns true if the given topic name is valid
|
||||
func AllowedTopic(username string) bool {
|
||||
return allowedTopicRegex.MatchString(username)
|
||||
func AllowedTopic(topic string) bool {
|
||||
return allowedTopicRegex.MatchString(topic)
|
||||
}
|
||||
|
||||
// AllowedTopicPattern returns true if the given topic pattern is valid; this includes the wildcard character (*)
|
||||
func AllowedTopicPattern(username string) bool {
|
||||
return allowedTopicPatternRegex.MatchString(username)
|
||||
func AllowedTopicPattern(topic string) bool {
|
||||
return allowedTopicPatternRegex.MatchString(topic)
|
||||
}
|
||||
|
||||
// AllowedTier returns true if the given tier name is valid
|
||||
func AllowedTier(tier string) bool {
|
||||
return allowedTierRegex.MatchString(tier)
|
||||
}
|
||||
|
||||
// Error constants used by the package
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue