Rename auth package to user; add extendToken feature

This commit is contained in:
binwiederhier 2022-12-25 11:41:38 -05:00
parent 3aac1b2715
commit d4c7ad4beb
14 changed files with 368 additions and 276 deletions

View file

@ -54,6 +54,7 @@ var (
errHTTPBadRequestMatrixPushkeyBaseURLMismatch = &errHTTP{40020, http.StatusBadRequest, "invalid request: push key must be prefixed with base URL", "https://ntfy.sh/docs/publish/#matrix-gateway"}
errHTTPBadRequestIconURLInvalid = &errHTTP{40021, http.StatusBadRequest, "invalid request: icon URL is invalid", "https://ntfy.sh/docs/publish/#icons"}
errHTTPBadRequestSignupNotEnabled = &errHTTP{40022, http.StatusBadRequest, "invalid request: signup not enabled", "https://ntfy.sh/docs/config"}
errHTTPBadRequestNoTokenProvided = &errHTTP{40023, http.StatusBadRequest, "invalid request: no token provided", ""}
errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", ""}
errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication"}
errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication"}

View file

@ -9,6 +9,7 @@ import (
"encoding/json"
"errors"
"fmt"
"heckel.io/ntfy/user"
"io"
"net"
"net/http"
@ -30,17 +31,17 @@ import (
"github.com/emersion/go-smtp"
"github.com/gorilla/websocket"
"golang.org/x/sync/errgroup"
"heckel.io/ntfy/auth"
"heckel.io/ntfy/util"
)
/*
TODO
expire tokens
auto-extend tokens from UI
use token auth in "SubscribeDialog"
upload files based on user limit
database migration
publishXHR + poll should pick current user, not from userManager
expire tokens
auto-refresh tokens from UI
reserve topics
purge accounts that were not logged into in X
sync subscription display name
@ -55,7 +56,11 @@ import (
Polishing:
aria-label for everything
Tests:
- APIs
- CRUD tokens
- Expire tokens
-
*/
// Server is the main server, providing the UI and API for ntfy
@ -71,7 +76,7 @@ type Server struct {
visitors map[string]*visitor // ip:<ip> or user:<user>
firebaseClient *firebaseClient
messages int64
auth auth.Manager
userManager user.Manager
messageCache *messageCache
fileCache *fileCache
closeChan chan bool
@ -159,9 +164,9 @@ func New(conf *Config) (*Server, error) {
return nil, err
}
}
var auther auth.Manager
var auther user.Manager
if conf.AuthFile != "" {
auther, err = auth.NewSQLiteAuthManager(conf.AuthFile, conf.AuthDefaultRead, conf.AuthDefaultWrite)
auther, err = user.NewSQLiteAuthManager(conf.AuthFile, conf.AuthDefaultRead, conf.AuthDefaultWrite)
if err != nil {
return nil, err
}
@ -181,7 +186,7 @@ func New(conf *Config) (*Server, error) {
firebaseClient: firebaseClient,
smtpSender: mailer,
topics: topics,
auth: auther,
userManager: auther,
visitors: make(map[string]*visitor),
}, nil
}
@ -342,11 +347,13 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
return s.handleAccountDelete(w, r, v)
} else if r.Method == http.MethodPost && r.URL.Path == accountPasswordPath {
return s.handleAccountPasswordChange(w, r, v)
} else if r.Method == http.MethodGet && r.URL.Path == accountTokenPath {
return s.handleAccountTokenGet(w, r, v)
} else if r.Method == http.MethodPost && r.URL.Path == accountTokenPath {
return s.handleAccountTokenIssue(w, r, v)
} else if r.Method == http.MethodPatch && r.URL.Path == accountTokenPath {
return s.handleAccountTokenExtend(w, r, v)
} else if r.Method == http.MethodDelete && r.URL.Path == accountTokenPath {
return s.handleAccountTokenDelete(w, r, v)
} else if r.Method == http.MethodPost && r.URL.Path == accountSettingsPath {
} else if r.Method == http.MethodPatch && r.URL.Path == accountSettingsPath {
return s.handleAccountSettingsChange(w, r, v)
} else if r.Method == http.MethodPost && r.URL.Path == accountSubscriptionPath {
return s.handleAccountSubscriptionAdd(w, r, v)
@ -557,7 +564,7 @@ func (s *Server) handlePublishWithoutResponse(r *http.Request, v *visitor) (*mes
}
v.IncrMessages()
if v.user != nil {
s.auth.EnqueueUpdateStats(v.user)
s.userManager.EnqueueStats(v.user)
}
s.mu.Lock()
s.messages++
@ -1122,7 +1129,7 @@ func parseSince(r *http.Request, poll bool) (sinceMarker, error) {
}
func (s *Server) handleOptions(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
w.Header().Set("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE")
w.Header().Set("Access-Control-Allow-Methods", "GET, PUT, POST, PATCH, DELETE")
w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests
w.Header().Set("Access-Control-Allow-Headers", "*") // CORS, allow auth via JS // FIXME is this terrible?
return nil
@ -1192,6 +1199,11 @@ func (s *Server) updateStatsAndPrune() {
s.mu.Unlock()
log.Debug("Manager: Deleted %d stale visitor(s)", staleVisitors)
// Delete expired user tokens
if err := s.userManager.RemoveExpiredTokens(); err != nil {
log.Warn("Error expiring user tokens: %s", err.Error())
}
// Delete expired attachments
if s.fileCache != nil && s.config.AttachmentExpiryDuration > 0 {
olderThan := time.Now().Add(-1 * s.config.AttachmentExpiryDuration)
@ -1323,7 +1335,7 @@ func (s *Server) sendDelayedMessages() error {
for _, m := range messages {
var v *visitor
if m.User != "" {
user, err := s.auth.User(m.User)
user, err := s.userManager.User(m.User)
if err != nil {
log.Warn("%s Error sending delayed message: %s", logMessagePrefix(v, m), err.Error())
continue
@ -1457,16 +1469,16 @@ func (s *Server) transformMatrixJSON(next handleFunc) handleFunc {
}
func (s *Server) authorizeTopicWrite(next handleFunc) handleFunc {
return s.autorizeTopic(next, auth.PermissionWrite)
return s.autorizeTopic(next, user.PermissionWrite)
}
func (s *Server) authorizeTopicRead(next handleFunc) handleFunc {
return s.autorizeTopic(next, auth.PermissionRead)
return s.autorizeTopic(next, user.PermissionRead)
}
func (s *Server) autorizeTopic(next handleFunc, perm auth.Permission) handleFunc {
func (s *Server) autorizeTopic(next handleFunc, perm user.Permission) handleFunc {
return func(w http.ResponseWriter, r *http.Request, v *visitor) error {
if s.auth == nil {
if s.userManager == nil {
return next(w, r, v)
}
topics, _, err := s.topicsFromPath(r.URL.Path)
@ -1474,7 +1486,7 @@ func (s *Server) autorizeTopic(next handleFunc, perm auth.Permission) handleFunc
return err
}
for _, t := range topics {
if err := s.auth.Authorize(v.user, t.ID, perm); err != nil {
if err := s.userManager.Authorize(v.user, t.ID, perm); err != nil {
log.Info("unauthorized: %s", err.Error())
return errHTTPForbidden
}
@ -1487,7 +1499,7 @@ func (s *Server) autorizeTopic(next handleFunc, perm auth.Permission) handleFunc
// Note that this function will always return a visitor, even if an error occurs.
func (s *Server) visitor(r *http.Request) (v *visitor, err error) {
ip := extractIPAddress(r, s.config.BehindProxy)
var user *auth.User // may stay nil if no auth header!
var user *user.User // may stay nil if no auth header!
if user, err = s.authenticate(r); err != nil {
log.Debug("authentication failed: %s", err.Error())
err = errHTTPUnauthorized // Always return visitor, even when error occurs!
@ -1505,7 +1517,7 @@ func (s *Server) visitor(r *http.Request) (v *visitor, err error) {
// The Authorization header can be passed as a header or the ?auth=... query param. The latter is required only to
// support the WebSocket JavaScript class, which does not support passing headers during the initial request. The auth
// query param is effectively double base64 encoded. Its format is base64(Basic base64(user:pass)).
func (s *Server) authenticate(r *http.Request) (user *auth.User, err error) {
func (s *Server) authenticate(r *http.Request) (user *user.User, err error) {
value := r.Header.Get("Authorization")
queryParam := readQueryParam(r, "authorization", "auth")
if queryParam != "" {
@ -1524,21 +1536,21 @@ func (s *Server) authenticate(r *http.Request) (user *auth.User, err error) {
return s.authenticateBasicAuth(r, value)
}
func (s *Server) authenticateBasicAuth(r *http.Request, value string) (user *auth.User, err error) {
func (s *Server) authenticateBasicAuth(r *http.Request, value string) (user *user.User, err error) {
r.Header.Set("Authorization", value)
username, password, ok := r.BasicAuth()
if !ok {
return nil, errors.New("invalid basic auth")
}
return s.auth.Authenticate(username, password)
return s.userManager.Authenticate(username, password)
}
func (s *Server) authenticateBearerAuth(value string) (user *auth.User, err error) {
func (s *Server) authenticateBearerAuth(value string) (user *user.User, err error) {
token := strings.TrimSpace(strings.TrimPrefix(value, "Bearer"))
return s.auth.AuthenticateToken(token)
return s.userManager.AuthenticateToken(token)
}
func (s *Server) visitorFromID(visitorID string, ip netip.Addr, user *auth.User) *visitor {
func (s *Server) visitorFromID(visitorID string, ip netip.Addr, user *user.User) *visitor {
s.mu.Lock()
defer s.mu.Unlock()
v, exists := s.visitors[visitorID]
@ -1554,6 +1566,6 @@ func (s *Server) visitorFromIP(ip netip.Addr) *visitor {
return s.visitorFromID(fmt.Sprintf("ip:%s", ip.String()), ip, nil)
}
func (s *Server) visitorFromUser(user *auth.User, ip netip.Addr) *visitor {
func (s *Server) visitorFromUser(user *user.User, ip netip.Addr) *visitor {
return s.visitorFromID(fmt.Sprintf("user:%s", user.Name), ip, user)
}

View file

@ -3,13 +3,13 @@ package server
import (
"encoding/json"
"errors"
"heckel.io/ntfy/auth"
"heckel.io/ntfy/user"
"heckel.io/ntfy/util"
"net/http"
)
func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v *visitor) error {
admin := v.user != nil && v.user.Role == auth.RoleAdmin
admin := v.user != nil && v.user.Role == user.RoleAdmin
if !admin {
if !s.config.EnableSignup {
return errHTTPBadRequestSignupNotEnabled
@ -26,13 +26,13 @@ func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v *
if err := json.NewDecoder(body).Decode(&newAccount); err != nil {
return err
}
if existingUser, _ := s.auth.User(newAccount.Username); existingUser != nil {
if existingUser, _ := s.userManager.User(newAccount.Username); existingUser != nil {
return errHTTPConflictUserExists
}
if v.accountLimiter != nil && !v.accountLimiter.Allow() {
return errHTTPTooManyRequestsAccountCreateLimit
}
if err := s.auth.AddUser(newAccount.Username, newAccount.Password, auth.RoleUser); err != nil { // TODO this should return a User
if err := s.userManager.AddUser(newAccount.Username, newAccount.Password, user.RoleUser); err != nil { // TODO this should return a User
return err
}
w.Header().Set("Content-Type", "application/json")
@ -84,23 +84,23 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, r *http.Request, v *vis
Code: v.user.Plan.Code,
Upgradable: v.user.Plan.Upgradable,
}
} else if v.user.Role == auth.RoleAdmin {
} else if v.user.Role == user.RoleAdmin {
response.Plan = &apiAccountPlan{
Code: string(auth.PlanUnlimited),
Code: string(user.PlanUnlimited),
Upgradable: false,
}
} else {
response.Plan = &apiAccountPlan{
Code: string(auth.PlanDefault),
Code: string(user.PlanDefault),
Upgradable: true,
}
}
} else {
response.Username = auth.Everyone
response.Role = string(auth.RoleAnonymous)
response.Username = user.Everyone
response.Role = string(user.RoleAnonymous)
response.Plan = &apiAccountPlan{
Code: string(auth.PlanNone),
Code: string(user.PlanNone),
Upgradable: true,
}
}
@ -114,7 +114,7 @@ func (s *Server) handleAccountDelete(w http.ResponseWriter, r *http.Request, v *
if v.user == nil {
return errHTTPUnauthorized
}
if err := s.auth.RemoveUser(v.user.Name); err != nil {
if err := s.userManager.RemoveUser(v.user.Name); err != nil {
return err
}
w.Header().Set("Content-Type", "application/json")
@ -136,7 +136,7 @@ func (s *Server) handleAccountPasswordChange(w http.ResponseWriter, r *http.Requ
if err := json.NewDecoder(body).Decode(&newPassword); err != nil {
return err
}
if err := s.auth.ChangePassword(v.user.Name, newPassword.Password); err != nil {
if err := s.userManager.ChangePassword(v.user.Name, newPassword.Password); err != nil {
return err
}
w.Header().Set("Content-Type", "application/json")
@ -145,19 +145,43 @@ func (s *Server) handleAccountPasswordChange(w http.ResponseWriter, r *http.Requ
return nil
}
func (s *Server) handleAccountTokenGet(w http.ResponseWriter, r *http.Request, v *visitor) error {
func (s *Server) handleAccountTokenIssue(w http.ResponseWriter, r *http.Request, v *visitor) error {
// TODO rate limit
if v.user == nil {
return errHTTPUnauthorized
}
token, err := s.auth.CreateToken(v.user)
token, err := s.userManager.CreateToken(v.user)
if err != nil {
return err
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
response := &apiAccountTokenResponse{
Token: token,
Token: token.Value,
Expires: token.Expires,
}
if err := json.NewEncoder(w).Encode(response); err != nil {
return err
}
return nil
}
func (s *Server) handleAccountTokenExtend(w http.ResponseWriter, r *http.Request, v *visitor) error {
// TODO rate limit
if v.user == nil {
return errHTTPUnauthorized
} else if v.user.Token == "" {
return errHTTPBadRequestNoTokenProvided
}
token, err := s.userManager.ExtendToken(v.user)
if err != nil {
return err
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
response := &apiAccountTokenResponse{
Token: token.Value,
Expires: token.Expires,
}
if err := json.NewEncoder(w).Encode(response); err != nil {
return err
@ -170,7 +194,7 @@ func (s *Server) handleAccountTokenDelete(w http.ResponseWriter, r *http.Request
if v.user == nil || v.user.Token == "" {
return errHTTPUnauthorized
}
if err := s.auth.RemoveToken(v.user); err != nil {
if err := s.userManager.RemoveToken(v.user); err != nil {
return err
}
w.Header().Set("Access-Control-Allow-Origin", "*") // FIXME remove this
@ -188,12 +212,12 @@ func (s *Server) handleAccountSettingsChange(w http.ResponseWriter, r *http.Requ
return err
}
defer r.Body.Close()
var newPrefs auth.UserPrefs
var newPrefs user.Prefs
if err := json.NewDecoder(body).Decode(&newPrefs); err != nil {
return err
}
if v.user.Prefs == nil {
v.user.Prefs = &auth.UserPrefs{}
v.user.Prefs = &user.Prefs{}
}
prefs := v.user.Prefs
if newPrefs.Language != "" {
@ -201,7 +225,7 @@ func (s *Server) handleAccountSettingsChange(w http.ResponseWriter, r *http.Requ
}
if newPrefs.Notification != nil {
if prefs.Notification == nil {
prefs.Notification = &auth.UserNotificationPrefs{}
prefs.Notification = &user.NotificationPrefs{}
}
if newPrefs.Notification.DeleteAfter > 0 {
prefs.Notification.DeleteAfter = newPrefs.Notification.DeleteAfter
@ -213,7 +237,7 @@ func (s *Server) handleAccountSettingsChange(w http.ResponseWriter, r *http.Requ
prefs.Notification.MinPriority = newPrefs.Notification.MinPriority
}
}
return s.auth.ChangeSettings(v.user)
return s.userManager.ChangeSettings(v.user)
}
func (s *Server) handleAccountSubscriptionAdd(w http.ResponseWriter, r *http.Request, v *visitor) error {
@ -227,12 +251,12 @@ func (s *Server) handleAccountSubscriptionAdd(w http.ResponseWriter, r *http.Req
return err
}
defer r.Body.Close()
var newSubscription auth.UserSubscription
var newSubscription user.Subscription
if err := json.NewDecoder(body).Decode(&newSubscription); err != nil {
return err
}
if v.user.Prefs == nil {
v.user.Prefs = &auth.UserPrefs{}
v.user.Prefs = &user.Prefs{}
}
newSubscription.ID = "" // Client cannot set ID
for _, subscription := range v.user.Prefs.Subscriptions {
@ -244,7 +268,7 @@ func (s *Server) handleAccountSubscriptionAdd(w http.ResponseWriter, r *http.Req
if newSubscription.ID == "" {
newSubscription.ID = util.RandomString(16)
v.user.Prefs.Subscriptions = append(v.user.Prefs.Subscriptions, &newSubscription)
if err := s.auth.ChangeSettings(v.user); err != nil {
if err := s.userManager.ChangeSettings(v.user); err != nil {
return err
}
}
@ -268,7 +292,7 @@ func (s *Server) handleAccountSubscriptionDelete(w http.ResponseWriter, r *http.
if v.user.Prefs == nil || v.user.Prefs.Subscriptions == nil {
return nil
}
newSubscriptions := make([]*auth.UserSubscription, 0)
newSubscriptions := make([]*user.Subscription, 0)
for _, subscription := range v.user.Prefs.Subscriptions {
if subscription.ID != subscriptionID {
newSubscriptions = append(newSubscriptions, subscription)
@ -276,7 +300,7 @@ func (s *Server) handleAccountSubscriptionDelete(w http.ResponseWriter, r *http.
}
if len(newSubscriptions) < len(v.user.Prefs.Subscriptions) {
v.user.Prefs.Subscriptions = newSubscriptions
if err := s.auth.ChangeSettings(v.user); err != nil {
if err := s.userManager.ChangeSettings(v.user); err != nil {
return err
}
}

View file

@ -8,8 +8,8 @@ import (
"firebase.google.com/go/v4/messaging"
"fmt"
"google.golang.org/api/option"
"heckel.io/ntfy/auth"
"heckel.io/ntfy/log"
"heckel.io/ntfy/user"
"heckel.io/ntfy/util"
"strings"
)
@ -28,10 +28,10 @@ var (
// The actual Firebase implementation is implemented in firebaseSenderImpl, to make it testable.
type firebaseClient struct {
sender firebaseSender
auther auth.Manager
auther user.Manager
}
func newFirebaseClient(sender firebaseSender, auther auth.Manager) *firebaseClient {
func newFirebaseClient(sender firebaseSender, auther user.Manager) *firebaseClient {
return &firebaseClient{
sender: sender,
auther: auther,
@ -112,7 +112,7 @@ func (c *firebaseSenderImpl) Send(m *messaging.Message) error {
// On Android, this will trigger the app to poll the topic and thereby displaying new messages.
// - If UpstreamBaseURL is set, messages are forwarded as poll requests to an upstream server and then forwarded
// to Firebase here. This is mainly for iOS to support self-hosted servers.
func toFirebaseMessage(m *message, auther auth.Manager) (*messaging.Message, error) {
func toFirebaseMessage(m *message, auther user.Manager) (*messaging.Message, error) {
var data map[string]string // Mostly matches https://ntfy.sh/docs/subscribe/api/#json-message-format
var apnsConfig *messaging.APNSConfig
switch m.Event {
@ -137,7 +137,7 @@ func toFirebaseMessage(m *message, auther auth.Manager) (*messaging.Message, err
case messageEvent:
allowForward := true
if auther != nil {
allowForward = auther.Authorize(nil, m.Topic, auth.PermissionRead) == nil
allowForward = auther.Authorize(nil, m.Topic, user.PermissionRead) == nil
}
if allowForward {
data = map[string]string{

View file

@ -11,18 +11,17 @@ import (
"firebase.google.com/go/v4/messaging"
"github.com/stretchr/testify/require"
"heckel.io/ntfy/auth"
)
type testAuther struct {
Allow bool
}
func (t testAuther) AuthenticateUser(_, _ string) (*auth.User, error) {
func (t testAuther) AuthenticateUser(_, _ string) (*user.User, error) {
return nil, errors.New("not used")
}
func (t testAuther) Authorize(_ *auth.User, _ string, _ auth.Permission) error {
func (t testAuther) Authorize(_ *user.User, _ string, _ user.Permission) error {
if t.Allow {
return nil
}

View file

@ -21,7 +21,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"heckel.io/ntfy/auth"
"heckel.io/ntfy/util"
)
@ -626,8 +625,8 @@ func TestServer_Auth_Success_Admin(t *testing.T) {
c.AuthFile = filepath.Join(t.TempDir(), "user.db")
s := newTestServer(t, c)
manager := s.auth.(auth.Manager)
require.Nil(t, manager.AddUser("phil", "phil", auth.RoleAdmin))
manager := s.userManager.(user.Manager)
require.Nil(t, manager.AddUser("phil", "phil", user.RoleAdmin))
response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{
"Authorization": basicAuth("phil:phil"),
@ -643,8 +642,8 @@ func TestServer_Auth_Success_User(t *testing.T) {
c.AuthDefaultWrite = false
s := newTestServer(t, c)
manager := s.auth.(auth.Manager)
require.Nil(t, manager.AddUser("ben", "ben", auth.RoleUser))
manager := s.userManager.(user.Manager)
require.Nil(t, manager.AddUser("ben", "ben", user.RoleUser))
require.Nil(t, manager.AllowAccess("ben", "mytopic", true, true))
response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{
@ -660,8 +659,8 @@ func TestServer_Auth_Success_User_MultipleTopics(t *testing.T) {
c.AuthDefaultWrite = false
s := newTestServer(t, c)
manager := s.auth.(auth.Manager)
require.Nil(t, manager.AddUser("ben", "ben", auth.RoleUser))
manager := s.userManager.(user.Manager)
require.Nil(t, manager.AddUser("ben", "ben", user.RoleUser))
require.Nil(t, manager.AllowAccess("ben", "mytopic", true, true))
require.Nil(t, manager.AllowAccess("ben", "anothertopic", true, true))
@ -683,8 +682,8 @@ func TestServer_Auth_Fail_InvalidPass(t *testing.T) {
c.AuthDefaultWrite = false
s := newTestServer(t, c)
manager := s.auth.(auth.Manager)
require.Nil(t, manager.AddUser("phil", "phil", auth.RoleAdmin))
manager := s.userManager.(user.Manager)
require.Nil(t, manager.AddUser("phil", "phil", user.RoleAdmin))
response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{
"Authorization": basicAuth("phil:INVALID"),
@ -699,8 +698,8 @@ func TestServer_Auth_Fail_Unauthorized(t *testing.T) {
c.AuthDefaultWrite = false
s := newTestServer(t, c)
manager := s.auth.(auth.Manager)
require.Nil(t, manager.AddUser("ben", "ben", auth.RoleUser))
manager := s.userManager.(user.Manager)
require.Nil(t, manager.AddUser("ben", "ben", user.RoleUser))
require.Nil(t, manager.AllowAccess("ben", "sometopic", true, true)) // Not mytopic!
response := request(t, s, "GET", "/mytopic/auth", "", map[string]string{
@ -716,10 +715,10 @@ func TestServer_Auth_Fail_CannotPublish(t *testing.T) {
c.AuthDefaultWrite = true // Open by default
s := newTestServer(t, c)
manager := s.auth.(auth.Manager)
require.Nil(t, manager.AddUser("phil", "phil", auth.RoleAdmin))
require.Nil(t, manager.AllowAccess(auth.Everyone, "private", false, false))
require.Nil(t, manager.AllowAccess(auth.Everyone, "announcements", true, false))
manager := s.userManager.(user.Manager)
require.Nil(t, manager.AddUser("phil", "phil", user.RoleAdmin))
require.Nil(t, manager.AllowAccess(user.Everyone, "private", false, false))
require.Nil(t, manager.AllowAccess(user.Everyone, "announcements", true, false))
response := request(t, s, "PUT", "/mytopic", "test", nil)
require.Equal(t, 200, response.Code)
@ -749,8 +748,8 @@ func TestServer_Auth_ViaQuery(t *testing.T) {
c.AuthDefaultWrite = false
s := newTestServer(t, c)
manager := s.auth.(auth.Manager)
require.Nil(t, manager.AddUser("ben", "some pass", auth.RoleAdmin))
manager := s.userManager.(user.Manager)
require.Nil(t, manager.AddUser("ben", "some pass", user.RoleAdmin))
u := fmt.Sprintf("/mytopic/json?poll=1&auth=%s", base64.RawURLEncoding.EncodeToString([]byte(basicAuth("ben:some pass"))))
response := request(t, s, "GET", u, "", nil)

View file

@ -1,7 +1,7 @@
package server
import (
"heckel.io/ntfy/auth"
"heckel.io/ntfy/user"
"net/http"
"net/netip"
"time"
@ -226,7 +226,8 @@ type apiAccountCreateRequest struct {
}
type apiAccountTokenResponse struct {
Token string `json:"token"`
Token string `json:"token"`
Expires int64 `json:"expires"`
}
type apiAccountPlan struct {
@ -252,12 +253,12 @@ type apiAccountStats struct {
}
type apiAccountSettingsResponse struct {
Username string `json:"username"`
Role string `json:"role,omitempty"`
Language string `json:"language,omitempty"`
Notification *auth.UserNotificationPrefs `json:"notification,omitempty"`
Subscriptions []*auth.UserSubscription `json:"subscriptions,omitempty"`
Plan *apiAccountPlan `json:"plan,omitempty"`
Limits *apiAccountLimits `json:"limits,omitempty"`
Stats *apiAccountStats `json:"stats,omitempty"`
Username string `json:"username"`
Role string `json:"role,omitempty"`
Language string `json:"language,omitempty"`
Notification *user.NotificationPrefs `json:"notification,omitempty"`
Subscriptions []*user.Subscription `json:"subscriptions,omitempty"`
Plan *apiAccountPlan `json:"plan,omitempty"`
Limits *apiAccountLimits `json:"limits,omitempty"`
Stats *apiAccountStats `json:"stats,omitempty"`
}

View file

@ -2,7 +2,7 @@ package server
import (
"errors"
"heckel.io/ntfy/auth"
"heckel.io/ntfy/user"
"net/netip"
"sync"
"time"
@ -27,7 +27,7 @@ type visitor struct {
config *Config
messageCache *messageCache
ip netip.Addr
user *auth.User
user *user.User
messages int64 // Number of messages sent
emails int64 // Number of emails sent
requestLimiter *rate.Limiter // Rate limiter for (almost) all requests (including messages)
@ -54,7 +54,7 @@ type visitorStats struct {
AttachmentFileSizeLimit int64
}
func newVisitor(conf *Config, messageCache *messageCache, ip netip.Addr, user *auth.User) *visitor {
func newVisitor(conf *Config, messageCache *messageCache, ip netip.Addr, user *user.User) *visitor {
var requestLimiter, emailsLimiter, accountLimiter *rate.Limiter
var messages, emails int64
if user != nil {
@ -171,7 +171,7 @@ func (v *visitor) Stats() (*visitorStats, error) {
emails := v.emails
v.mu.Unlock()
stats := &visitorStats{}
if v.user != nil && v.user.Role == auth.RoleAdmin {
if v.user != nil && v.user.Role == user.RoleAdmin {
stats.Basis = "role"
stats.MessagesLimit = 0
stats.EmailsLimit = 0