Payments webhook test, delete attachments/messages when reservations are removed,

This commit is contained in:
binwiederhier 2023-01-20 22:47:37 -05:00
parent 45b97c7054
commit 31a3bb7cd6
16 changed files with 571 additions and 157 deletions

View file

@ -219,8 +219,7 @@ const (
INSERT INTO tier (code, name, messages_limit, messages_expiry_duration, emails_limit, reservations_limit, attachment_file_size_limit, attachment_total_size_limit, attachment_expiry_duration, stripe_price_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`
selectTierIDQuery = `SELECT id FROM tier WHERE code = ?`
selectTiersQuery = `
selectTiersQuery = `
SELECT code, name, messages_limit, messages_expiry_duration, emails_limit, reservations_limit, attachment_file_size_limit, attachment_total_size_limit, attachment_expiry_duration, stripe_price_id
FROM tier
`
@ -234,7 +233,7 @@ const (
FROM tier
WHERE stripe_price_id = ?
`
updateUserTierQuery = `UPDATE user SET tier_id = ? WHERE user = ?`
updateUserTierQuery = `UPDATE user SET tier_id = (SELECT id FROM tier WHERE code = ?) WHERE user = ?`
deleteUserTierQuery = `UPDATE user SET tier_id = null WHERE user = ?`
updateBillingQuery = `
@ -772,26 +771,47 @@ func (a *Manager) ChangeRole(username string, role Role) error {
return nil
}
// ChangeTier changes a user's tier using the tier code
// ChangeTier changes a user's tier using the tier code. This function does not delete reservations, messages,
// or attachments, even if the new tier has lower limits in this regard. That has to be done elsewhere.
func (a *Manager) ChangeTier(username, tier string) error {
if !AllowedUsername(username) {
return ErrInvalidArgument
}
rows, err := a.db.Query(selectTierIDQuery, tier)
t, err := a.Tier(tier)
if err != nil {
return err
} else if err := a.checkReservationsLimit(username, t.ReservationsLimit); err != nil {
return err
}
if _, err := a.db.Exec(updateUserTierQuery, tier, username); err != nil {
return err
}
return nil
}
// ResetTier removes the tier from the given user
func (a *Manager) ResetTier(username string) error {
if !AllowedUsername(username) && username != Everyone && username != "" {
return ErrInvalidArgument
} else if err := a.checkReservationsLimit(username, 0); err != nil {
return err
}
_, err := a.db.Exec(deleteUserTierQuery, username)
return err
}
func (a *Manager) checkReservationsLimit(username string, reservationsLimit int64) error {
u, err := a.User(username)
if err != nil {
return err
}
defer rows.Close()
if !rows.Next() {
return ErrInvalidArgument
}
var tierID int64
if err := rows.Scan(&tierID); err != nil {
return err
}
rows.Close()
if _, err := a.db.Exec(updateUserTierQuery, tierID, username); err != nil {
return err
if u.Tier != nil && reservationsLimit < u.Tier.ReservationsLimit {
reservations, err := a.Reservations(username)
if err != nil {
return err
} else if int64(len(reservations)) > reservationsLimit {
return ErrTooManyReservations
}
}
return nil
}
@ -823,20 +843,37 @@ func (a *Manager) CheckAllowAccess(username string, topic string) error {
// 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 {
func (a *Manager) AllowAccess(username string, topicPattern string, permission Permission) 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 {
owner := ""
if _, err := a.db.Exec(upsertUserAccessQuery, username, toSQLWildcard(topicPattern), permission.IsRead(), permission.IsWrite(), owner, owner); err != nil {
return err
}
return nil
}
func (a *Manager) ReserveAccess(username string, topic string, everyone Permission) error {
if !AllowedUsername(username) || username == Everyone || !AllowedTopic(topic) {
return ErrInvalidArgument
}
tx, err := a.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if _, err := tx.Exec(upsertUserAccessQuery, username, topic, true, true, username, username); err != nil {
return err
}
if _, err := tx.Exec(upsertUserAccessQuery, Everyone, topic, everyone.IsRead(), everyone.IsWrite(), username, username); err != nil {
return err
}
return tx.Commit()
}
// 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 (*).
func (a *Manager) ResetAccess(username string, topicPattern string) error {
@ -856,13 +893,29 @@ func (a *Manager) ResetAccess(username string, topicPattern string) error {
return err
}
// ResetTier removes the tier from the given user
func (a *Manager) ResetTier(username string) error {
if !AllowedUsername(username) && username != Everyone && username != "" {
func (a *Manager) RemoveReservations(username string, topics ...string) error {
if !AllowedUsername(username) || username == Everyone || len(topics) == 0 {
return ErrInvalidArgument
}
_, err := a.db.Exec(deleteUserTierQuery, username)
return err
for _, topic := range topics {
if !AllowedTopic(topic) {
return ErrInvalidArgument
}
}
tx, err := a.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
for _, topic := range topics {
if _, err := tx.Exec(deleteTopicAccessQuery, username, username, topic); err != nil {
return err
}
if _, err := tx.Exec(deleteTopicAccessQuery, Everyone, Everyone, topic); err != nil {
return err
}
}
return tx.Commit()
}
// DefaultAccess returns the default read/write access if no access control entry matches
@ -879,8 +932,8 @@ func (a *Manager) CreateTier(tier *Tier) error {
}
// ChangeBilling updates a user's billing fields, namely the Stripe customer ID, and subscription information
func (a *Manager) ChangeBilling(user *User) error {
if _, err := a.db.Exec(updateBillingQuery, nullString(user.Billing.StripeCustomerID), nullString(user.Billing.StripeSubscriptionID), nullString(string(user.Billing.StripeSubscriptionStatus)), nullInt64(user.Billing.StripeSubscriptionPaidUntil.Unix()), nullInt64(user.Billing.StripeSubscriptionCancelAt.Unix()), user.Name); err != nil {
func (a *Manager) ChangeBilling(username string, billing *Billing) error {
if _, err := a.db.Exec(updateBillingQuery, nullString(billing.StripeCustomerID), nullString(billing.StripeSubscriptionID), nullString(string(billing.StripeSubscriptionStatus)), nullInt64(billing.StripeSubscriptionPaidUntil.Unix()), nullInt64(billing.StripeSubscriptionCancelAt.Unix()), username); err != nil {
return err
}
return nil

View file

@ -15,13 +15,13 @@ func TestManager_FullScenario_Default_DenyAll(t *testing.T) {
a := newTestManager(t, PermissionDenyAll)
require.Nil(t, a.AddUser("phil", "phil", RoleAdmin, "unit-test"))
require.Nil(t, a.AddUser("ben", "ben", RoleUser, "unit-test"))
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", PermissionReadWrite))
require.Nil(t, a.AllowAccess("ben", "readme", PermissionRead))
require.Nil(t, a.AllowAccess("ben", "writeme", PermissionWrite))
require.Nil(t, a.AllowAccess("ben", "everyonewrite", PermissionDenyAll)) // How unfair!
require.Nil(t, a.AllowAccess(Everyone, "announcements", PermissionRead))
require.Nil(t, a.AllowAccess(Everyone, "everyonewrite", PermissionReadWrite))
require.Nil(t, a.AllowAccess(Everyone, "up*", PermissionWrite)) // Everyone can write to /up*
phil, err := a.Authenticate("phil", "phil")
require.Nil(t, err)
@ -130,12 +130,12 @@ func TestManager_UserManagement(t *testing.T) {
a := newTestManager(t, PermissionDenyAll)
require.Nil(t, a.AddUser("phil", "phil", RoleAdmin, "unit-test"))
require.Nil(t, a.AddUser("ben", "ben", RoleUser, "unit-test"))
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", PermissionReadWrite))
require.Nil(t, a.AllowAccess("ben", "readme", PermissionRead))
require.Nil(t, a.AllowAccess("ben", "writeme", PermissionWrite))
require.Nil(t, a.AllowAccess("ben", "everyonewrite", PermissionDenyAll)) // How unfair!
require.Nil(t, a.AllowAccess(Everyone, "announcements", PermissionRead))
require.Nil(t, a.AllowAccess(Everyone, "everyonewrite", PermissionReadWrite))
// Query user details
phil, err := a.User("phil")
@ -177,9 +177,9 @@ func TestManager_UserManagement(t *testing.T) {
}, everyoneGrants)
// 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", PermissionReadWrite)) // Overwrite!
require.Nil(t, a.AllowAccess("ben", "readme", PermissionRead))
require.Nil(t, a.AllowAccess("ben", "writeme", PermissionWrite))
require.Nil(t, a.Authorize(ben, "mytopic", PermissionRead))
require.Nil(t, a.Authorize(ben, "mytopic", PermissionWrite))
require.Nil(t, a.Authorize(ben, "readme", PermissionRead))
@ -234,8 +234,8 @@ func TestManager_ChangePassword(t *testing.T) {
func TestManager_ChangeRole(t *testing.T) {
a := newTestManager(t, PermissionDenyAll)
require.Nil(t, a.AddUser("ben", "ben", RoleUser, "unit-test"))
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", PermissionReadWrite))
require.Nil(t, a.AllowAccess("ben", "readme", PermissionRead))
ben, err := a.User("ben")
require.Nil(t, err)
@ -256,6 +256,28 @@ func TestManager_ChangeRole(t *testing.T) {
require.Equal(t, 0, len(benGrants))
}
func TestManager_Reservations(t *testing.T) {
a := newTestManager(t, PermissionDenyAll)
require.Nil(t, a.AddUser("ben", "ben", RoleUser, "unit-test"))
require.Nil(t, a.ReserveAccess("ben", "ztopic", PermissionDenyAll))
require.Nil(t, a.ReserveAccess("ben", "readme", PermissionRead))
require.Nil(t, a.AllowAccess("ben", "something-else", PermissionRead))
reservations, err := a.Reservations("ben")
require.Nil(t, err)
require.Equal(t, 2, len(reservations))
require.Equal(t, Reservation{
Topic: "readme",
Owner: PermissionReadWrite,
Everyone: PermissionRead,
}, reservations[0])
require.Equal(t, Reservation{
Topic: "ztopic",
Owner: PermissionReadWrite,
Everyone: PermissionDenyAll,
}, reservations[1])
}
func TestManager_ChangeRoleFromTierUserToAdmin(t *testing.T) {
a := newTestManager(t, PermissionDenyAll)
require.Nil(t, a.CreateTier(&Tier{
@ -272,8 +294,7 @@ func TestManager_ChangeRoleFromTierUserToAdmin(t *testing.T) {
}))
require.Nil(t, a.AddUser("ben", "ben", RoleUser, "unit-test"))
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))
require.Nil(t, a.ReserveAccess("ben", "mytopic", PermissionDenyAll))
ben, err := a.User("ben")
require.Nil(t, err)
@ -298,6 +319,13 @@ func TestManager_ChangeRoleFromTierUserToAdmin(t *testing.T) {
require.Equal(t, 1, len(everyoneGrants))
require.Equal(t, PermissionDenyAll, everyoneGrants[0].Allow)
benReservations, err := a.Reservations("ben")
require.Nil(t, err)
require.Equal(t, 1, len(benReservations))
require.Equal(t, "mytopic", benReservations[0].Topic)
require.Equal(t, PermissionReadWrite, benReservations[0].Owner)
require.Equal(t, PermissionDenyAll, benReservations[0].Everyone)
// Switch to admin, this should remove all grants and owned ACL entries
require.Nil(t, a.ChangeRole("ben", RoleAdmin))

View file

@ -221,19 +221,10 @@ func AllowedTier(tier string) bool {
// Error constants used by the package
var (
ErrUnauthenticated = errors.New("unauthenticated")
ErrUnauthorized = errors.New("unauthorized")
ErrInvalidArgument = errors.New("invalid argument")
ErrUserNotFound = errors.New("user not found")
ErrTierNotFound = errors.New("tier not found")
)
// BillingStatus represents the status of a Stripe subscription
type BillingStatus string
// BillingStatus values, subset of https://stripe.com/docs/billing/subscriptions/overview
const (
BillingStatusIncomplete = BillingStatus("incomplete")
BillingStatusActive = BillingStatus("active")
BillingStatusPastDue = BillingStatus("past_due")
ErrUnauthenticated = errors.New("unauthenticated")
ErrUnauthorized = errors.New("unauthorized")
ErrInvalidArgument = errors.New("invalid argument")
ErrUserNotFound = errors.New("user not found")
ErrTierNotFound = errors.New("tier not found")
ErrTooManyReservations = errors.New("new tier has lower reservation limit")
)