types.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. // Package user deals with authentication and authorization against topics
  2. package user
  3. import (
  4. "errors"
  5. "github.com/stripe/stripe-go/v74"
  6. "regexp"
  7. "time"
  8. )
  9. // User is a struct that represents a user
  10. type User struct {
  11. ID string
  12. Name string
  13. Hash string // password hash (bcrypt)
  14. Token string // Only set if token was used to log in
  15. Role Role
  16. Prefs *Prefs
  17. Tier *Tier
  18. Stats *Stats
  19. Billing *Billing
  20. SyncTopic string
  21. Deleted bool
  22. }
  23. // Auther is an interface for authentication and authorization
  24. type Auther interface {
  25. // Authenticate checks username and password and returns a user if correct. The method
  26. // returns in constant-ish time, regardless of whether the user exists or the password is
  27. // correct or incorrect.
  28. Authenticate(username, password string) (*User, error)
  29. // Authorize returns nil if the given user has access to the given topic using the desired
  30. // permission. The user param may be nil to signal an anonymous user.
  31. Authorize(user *User, topic string, perm Permission) error
  32. }
  33. // Token represents a user token, including expiry date
  34. type Token struct {
  35. Value string
  36. Expires time.Time
  37. }
  38. // Prefs represents a user's configuration settings
  39. type Prefs struct {
  40. Language string `json:"language,omitempty"`
  41. Notification *NotificationPrefs `json:"notification,omitempty"`
  42. Subscriptions []*Subscription `json:"subscriptions,omitempty"`
  43. }
  44. // Tier represents a user's account type, including its account limits
  45. type Tier struct {
  46. ID string
  47. Code string
  48. Name string
  49. Paid bool
  50. MessagesLimit int64
  51. MessagesExpiryDuration time.Duration
  52. EmailsLimit int64
  53. ReservationsLimit int64
  54. AttachmentFileSizeLimit int64
  55. AttachmentTotalSizeLimit int64
  56. AttachmentExpiryDuration time.Duration
  57. StripePriceID string
  58. }
  59. // Subscription represents a user's topic subscription
  60. type Subscription struct {
  61. ID string `json:"id"`
  62. BaseURL string `json:"base_url"`
  63. Topic string `json:"topic"`
  64. DisplayName string `json:"display_name"`
  65. }
  66. // NotificationPrefs represents the user's notification settings
  67. type NotificationPrefs struct {
  68. Sound string `json:"sound,omitempty"`
  69. MinPriority int `json:"min_priority,omitempty"`
  70. DeleteAfter int `json:"delete_after,omitempty"`
  71. }
  72. // Stats is a struct holding daily user statistics
  73. type Stats struct {
  74. Messages int64
  75. Emails int64
  76. }
  77. // Billing is a struct holding a user's billing information
  78. type Billing struct {
  79. StripeCustomerID string
  80. StripeSubscriptionID string
  81. StripeSubscriptionStatus stripe.SubscriptionStatus
  82. StripeSubscriptionPaidUntil time.Time
  83. StripeSubscriptionCancelAt time.Time
  84. }
  85. // Grant is a struct that represents an access control entry to a topic by a user
  86. type Grant struct {
  87. TopicPattern string // May include wildcard (*)
  88. Allow Permission
  89. }
  90. // Reservation is a struct that represents the ownership over a topic by a user
  91. type Reservation struct {
  92. Topic string
  93. Owner Permission
  94. Everyone Permission
  95. }
  96. // Permission represents a read or write permission to a topic
  97. type Permission uint8
  98. // Permissions to a topic
  99. const (
  100. PermissionDenyAll Permission = iota
  101. PermissionRead
  102. PermissionWrite
  103. PermissionReadWrite // 3!
  104. )
  105. // NewPermission is a helper to create a Permission based on read/write bool values
  106. func NewPermission(read, write bool) Permission {
  107. p := uint8(0)
  108. if read {
  109. p |= uint8(PermissionRead)
  110. }
  111. if write {
  112. p |= uint8(PermissionWrite)
  113. }
  114. return Permission(p)
  115. }
  116. // ParsePermission parses the string representation and returns a Permission
  117. func ParsePermission(s string) (Permission, error) {
  118. switch s {
  119. case "read-write", "rw":
  120. return NewPermission(true, true), nil
  121. case "read-only", "read", "ro":
  122. return NewPermission(true, false), nil
  123. case "write-only", "write", "wo":
  124. return NewPermission(false, true), nil
  125. case "deny-all", "deny", "none":
  126. return NewPermission(false, false), nil
  127. default:
  128. return NewPermission(false, false), errors.New("invalid permission")
  129. }
  130. }
  131. // IsRead returns true if readable
  132. func (p Permission) IsRead() bool {
  133. return p&PermissionRead != 0
  134. }
  135. // IsWrite returns true if writable
  136. func (p Permission) IsWrite() bool {
  137. return p&PermissionWrite != 0
  138. }
  139. // IsReadWrite returns true if readable and writable
  140. func (p Permission) IsReadWrite() bool {
  141. return p.IsRead() && p.IsWrite()
  142. }
  143. // String returns a string representation of the permission
  144. func (p Permission) String() string {
  145. if p.IsReadWrite() {
  146. return "read-write"
  147. } else if p.IsRead() {
  148. return "read-only"
  149. } else if p.IsWrite() {
  150. return "write-only"
  151. }
  152. return "deny-all"
  153. }
  154. // Role represents a user's role, either admin or regular user
  155. type Role string
  156. // User roles
  157. const (
  158. RoleAdmin = Role("admin") // Some queries have these values hardcoded!
  159. RoleUser = Role("user")
  160. RoleAnonymous = Role("anonymous")
  161. )
  162. // Everyone is a special username representing anonymous users
  163. const (
  164. Everyone = "*"
  165. everyoneID = "u_everyone"
  166. )
  167. var (
  168. allowedUsernameRegex = regexp.MustCompile(`^[-_.@a-zA-Z0-9]+$`) // Does not include Everyone (*)
  169. allowedTopicRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`) // No '*'
  170. allowedTopicPatternRegex = regexp.MustCompile(`^[-_*A-Za-z0-9]{1,64}$`) // Adds '*' for wildcards!
  171. allowedTierRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`)
  172. )
  173. // AllowedRole returns true if the given role can be used for new users
  174. func AllowedRole(role Role) bool {
  175. return role == RoleUser || role == RoleAdmin
  176. }
  177. // AllowedUsername returns true if the given username is valid
  178. func AllowedUsername(username string) bool {
  179. return allowedUsernameRegex.MatchString(username)
  180. }
  181. // AllowedTopic returns true if the given topic name is valid
  182. func AllowedTopic(topic string) bool {
  183. return allowedTopicRegex.MatchString(topic)
  184. }
  185. // AllowedTopicPattern returns true if the given topic pattern is valid; this includes the wildcard character (*)
  186. func AllowedTopicPattern(topic string) bool {
  187. return allowedTopicPatternRegex.MatchString(topic)
  188. }
  189. // AllowedTier returns true if the given tier name is valid
  190. func AllowedTier(tier string) bool {
  191. return allowedTierRegex.MatchString(tier)
  192. }
  193. // Error constants used by the package
  194. var (
  195. ErrUnauthenticated = errors.New("unauthenticated")
  196. ErrUnauthorized = errors.New("unauthorized")
  197. ErrInvalidArgument = errors.New("invalid argument")
  198. ErrUserNotFound = errors.New("user not found")
  199. ErrTierNotFound = errors.New("tier not found")
  200. ErrTooManyReservations = errors.New("new tier has lower reservation limit")
  201. )