Replace read/write flags with Permission

This commit is contained in:
binwiederhier 2023-01-02 21:12:42 -05:00
parent 1733323132
commit 4b9d40464c
13 changed files with 194 additions and 152 deletions

View file

@ -192,11 +192,11 @@ func showUsers(c *cli.Context, manager *user.Manager, users []*user.User) error
fmt.Fprintf(c.App.ErrWriter, "- read-write access to all topics (admin role)\n") fmt.Fprintf(c.App.ErrWriter, "- read-write access to all topics (admin role)\n")
} else if len(grants) > 0 { } else if len(grants) > 0 {
for _, grant := range grants { for _, grant := range grants {
if grant.AllowRead && grant.AllowWrite { if grant.Allow.IsReadWrite() {
fmt.Fprintf(c.App.ErrWriter, "- read-write access to topic %s\n", grant.TopicPattern) fmt.Fprintf(c.App.ErrWriter, "- read-write access to topic %s\n", grant.TopicPattern)
} else if grant.AllowRead { } else if grant.Allow.IsRead() {
fmt.Fprintf(c.App.ErrWriter, "- read-only access to topic %s\n", grant.TopicPattern) fmt.Fprintf(c.App.ErrWriter, "- read-only access to topic %s\n", grant.TopicPattern)
} else if grant.AllowWrite { } else if grant.Allow.IsWrite() {
fmt.Fprintf(c.App.ErrWriter, "- write-only access to topic %s\n", grant.TopicPattern) fmt.Fprintf(c.App.ErrWriter, "- write-only access to topic %s\n", grant.TopicPattern)
} else { } else {
fmt.Fprintf(c.App.ErrWriter, "- no access to topic %s\n", grant.TopicPattern) fmt.Fprintf(c.App.ErrWriter, "- no access to topic %s\n", grant.TopicPattern)
@ -206,12 +206,12 @@ func showUsers(c *cli.Context, manager *user.Manager, users []*user.User) error
fmt.Fprintf(c.App.ErrWriter, "- no topic-specific permissions\n") fmt.Fprintf(c.App.ErrWriter, "- no topic-specific permissions\n")
} }
if u.Name == user.Everyone { if u.Name == user.Everyone {
defaultRead, defaultWrite := manager.DefaultAccess() access := manager.DefaultAccess()
if defaultRead && defaultWrite { if access.IsReadWrite() {
fmt.Fprintln(c.App.ErrWriter, "- read-write access to all (other) topics (server config)") fmt.Fprintln(c.App.ErrWriter, "- read-write access to all (other) topics (server config)")
} else if defaultRead { } else if access.IsRead() {
fmt.Fprintln(c.App.ErrWriter, "- read-only access to all (other) topics (server config)") fmt.Fprintln(c.App.ErrWriter, "- read-only access to all (other) topics (server config)")
} else if defaultWrite { } else if access.IsWrite() {
fmt.Fprintln(c.App.ErrWriter, "- write-only access to all (other) topics (server config)") fmt.Fprintln(c.App.ErrWriter, "- write-only access to all (other) topics (server config)")
} else { } else {
fmt.Fprintln(c.App.ErrWriter, "- no access to any (other) topics (server config)") fmt.Fprintln(c.App.ErrWriter, "- no access to any (other) topics (server config)")

View file

@ -81,7 +81,7 @@ func runAccessCommand(app *cli.App, conf *server.Config, args ...string) error {
"ntfy", "ntfy",
"access", "access",
"--auth-file=" + conf.AuthFile, "--auth-file=" + conf.AuthFile,
"--auth-default-access=" + confToDefaultAccess(conf), "--auth-default-access=" + conf.AuthDefault.String(),
} }
return app.Run(append(userArgs, args...)) return app.Run(append(userArgs, args...))
} }

View file

@ -5,6 +5,7 @@ package cmd
import ( import (
"errors" "errors"
"fmt" "fmt"
"heckel.io/ntfy/user"
"io/fs" "io/fs"
"math" "math"
"net" "net"
@ -171,8 +172,6 @@ func execServe(c *cli.Context) error {
return errors.New("if set, base-url must start with http:// or https://") return errors.New("if set, base-url must start with http:// or https://")
} else if baseURL != "" && strings.HasSuffix(baseURL, "/") { } else if baseURL != "" && strings.HasSuffix(baseURL, "/") {
return errors.New("if set, base-url must not end with a slash (/)") return errors.New("if set, base-url must not end with a slash (/)")
} else if !util.Contains([]string{"read-write", "read-only", "write-only", "deny-all"}, authDefaultAccess) {
return errors.New("if set, auth-default-access must start set to 'read-write', 'read-only', 'write-only' or 'deny-all'")
} else if !util.Contains([]string{"app", "home", "disable"}, webRoot) { } else if !util.Contains([]string{"app", "home", "disable"}, webRoot) {
return errors.New("if set, web-root must be 'home' or 'app'") return errors.New("if set, web-root must be 'home' or 'app'")
} else if upstreamBaseURL != "" && !strings.HasPrefix(upstreamBaseURL, "http://") && !strings.HasPrefix(upstreamBaseURL, "https://") { } else if upstreamBaseURL != "" && !strings.HasPrefix(upstreamBaseURL, "http://") && !strings.HasPrefix(upstreamBaseURL, "https://") {
@ -189,8 +188,10 @@ func execServe(c *cli.Context) error {
enableWeb := webRoot != "disable" enableWeb := webRoot != "disable"
// Default auth permissions // Default auth permissions
authDefaultRead := authDefaultAccess == "read-write" || authDefaultAccess == "read-only" authDefault, err := user.ParsePermission(authDefaultAccess)
authDefaultWrite := authDefaultAccess == "read-write" || authDefaultAccess == "write-only" if err != nil {
return errors.New("if set, auth-default-access must start set to 'read-write', 'read-only', 'write-only' or 'deny-all'")
}
// Special case: Unset default // Special case: Unset default
if listenHTTP == "-" { if listenHTTP == "-" {
@ -244,8 +245,7 @@ func execServe(c *cli.Context) error {
conf.CacheBatchSize = cacheBatchSize conf.CacheBatchSize = cacheBatchSize
conf.CacheBatchTimeout = cacheBatchTimeout conf.CacheBatchTimeout = cacheBatchTimeout
conf.AuthFile = authFile conf.AuthFile = authFile
conf.AuthDefaultRead = authDefaultRead conf.AuthDefault = authDefault
conf.AuthDefaultWrite = authDefaultWrite
conf.AttachmentCacheDir = attachmentCacheDir conf.AttachmentCacheDir = attachmentCacheDir
conf.AttachmentTotalSizeLimit = attachmentTotalSizeLimit conf.AttachmentTotalSizeLimit = attachmentTotalSizeLimit
conf.AttachmentFileSizeLimit = attachmentFileSizeLimit conf.AttachmentFileSizeLimit = attachmentFileSizeLimit

View file

@ -273,12 +273,12 @@ func createUserManager(c *cli.Context) (*user.Manager, error) {
return nil, errors.New("option auth-file not set; auth is unconfigured for this server") return nil, errors.New("option auth-file not set; auth is unconfigured for this server")
} else if !util.FileExists(authFile) { } else if !util.FileExists(authFile) {
return nil, errors.New("auth-file does not exist; please start the server at least once to create it") return nil, errors.New("auth-file does not exist; please start the server at least once to create it")
} else if !util.Contains([]string{"read-write", "read-only", "write-only", "deny-all"}, authDefaultAccess) {
return nil, errors.New("if set, auth-default-access must start set to 'read-write', 'read-only' or 'deny-all'")
} }
authDefaultRead := authDefaultAccess == "read-write" || authDefaultAccess == "read-only" authDefault, err := user.ParsePermission(authDefaultAccess)
authDefaultWrite := authDefaultAccess == "read-write" || authDefaultAccess == "write-only" if err != nil {
return user.NewManager(authFile, authDefaultRead, authDefaultWrite) return nil, errors.New("if set, auth-default-access must start set to 'read-write', 'read-only', 'write-only' or 'deny-all'")
}
return user.NewManager(authFile, authDefault)
} }
func readPasswordAndConfirm(c *cli.Context) (string, error) { func readPasswordAndConfirm(c *cli.Context) (string, error) {

View file

@ -5,6 +5,7 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"heckel.io/ntfy/server" "heckel.io/ntfy/server"
"heckel.io/ntfy/test" "heckel.io/ntfy/test"
"heckel.io/ntfy/user"
"path/filepath" "path/filepath"
"testing" "testing"
) )
@ -114,8 +115,7 @@ func TestCLI_User_Delete(t *testing.T) {
func newTestServerWithAuth(t *testing.T) (s *server.Server, conf *server.Config, port int) { func newTestServerWithAuth(t *testing.T) (s *server.Server, conf *server.Config, port int) {
conf = server.NewConfig() conf = server.NewConfig()
conf.AuthFile = filepath.Join(t.TempDir(), "user.db") conf.AuthFile = filepath.Join(t.TempDir(), "user.db")
conf.AuthDefaultRead = false conf.AuthDefault = user.PermissionDenyAll
conf.AuthDefaultWrite = false
s, port = test.StartServerWithConfig(t, conf) s, port = test.StartServerWithConfig(t, conf)
return return
} }
@ -125,21 +125,7 @@ func runUserCommand(app *cli.App, conf *server.Config, args ...string) error {
"ntfy", "ntfy",
"user", "user",
"--auth-file=" + conf.AuthFile, "--auth-file=" + conf.AuthFile,
"--auth-default-access=" + confToDefaultAccess(conf), "--auth-default-access=" + conf.AuthDefault.String(),
} }
return app.Run(append(userArgs, args...)) return app.Run(append(userArgs, args...))
} }
func confToDefaultAccess(conf *server.Config) string {
var defaultAccess string
if conf.AuthDefaultRead && conf.AuthDefaultWrite {
defaultAccess = "read-write"
} else if conf.AuthDefaultRead && !conf.AuthDefaultWrite {
defaultAccess = "read-only"
} else if !conf.AuthDefaultRead && conf.AuthDefaultWrite {
defaultAccess = "write-only"
} else if !conf.AuthDefaultRead && !conf.AuthDefaultWrite {
defaultAccess = "deny-all"
}
return defaultAccess
}

View file

@ -1,6 +1,7 @@
package server package server
import ( import (
"heckel.io/ntfy/user"
"io/fs" "io/fs"
"net/netip" "net/netip"
"time" "time"
@ -66,8 +67,7 @@ type Config struct {
CacheBatchSize int CacheBatchSize int
CacheBatchTimeout time.Duration CacheBatchTimeout time.Duration
AuthFile string AuthFile string
AuthDefaultRead bool AuthDefault user.Permission
AuthDefaultWrite bool
AttachmentCacheDir string AttachmentCacheDir string
AttachmentTotalSizeLimit int64 AttachmentTotalSizeLimit int64
AttachmentFileSizeLimit int64 AttachmentFileSizeLimit int64
@ -127,8 +127,7 @@ func NewConfig() *Config {
CacheBatchSize: 0, CacheBatchSize: 0,
CacheBatchTimeout: 0, CacheBatchTimeout: 0,
AuthFile: "", AuthFile: "",
AuthDefaultRead: true, AuthDefault: user.NewPermission(true, true),
AuthDefaultWrite: true,
AttachmentCacheDir: "", AttachmentCacheDir: "",
AttachmentTotalSizeLimit: DefaultAttachmentTotalSizeLimit, AttachmentTotalSizeLimit: DefaultAttachmentTotalSizeLimit,
AttachmentFileSizeLimit: DefaultAttachmentFileSizeLimit, AttachmentFileSizeLimit: DefaultAttachmentFileSizeLimit,

View file

@ -41,8 +41,8 @@ var (
errHTTPBadRequestDelayTooLarge = &errHTTP{40006, http.StatusBadRequest, "invalid delay parameter: too large, please refer to the docs", "https://ntfy.sh/docs/publish/#scheduled-delivery"} errHTTPBadRequestDelayTooLarge = &errHTTP{40006, http.StatusBadRequest, "invalid delay parameter: too large, please refer to the docs", "https://ntfy.sh/docs/publish/#scheduled-delivery"}
errHTTPBadRequestPriorityInvalid = &errHTTP{40007, http.StatusBadRequest, "invalid priority parameter", "https://ntfy.sh/docs/publish/#message-priority"} errHTTPBadRequestPriorityInvalid = &errHTTP{40007, http.StatusBadRequest, "invalid priority parameter", "https://ntfy.sh/docs/publish/#message-priority"}
errHTTPBadRequestSinceInvalid = &errHTTP{40008, http.StatusBadRequest, "invalid since parameter", "https://ntfy.sh/docs/subscribe/api/#fetch-cached-messages"} errHTTPBadRequestSinceInvalid = &errHTTP{40008, http.StatusBadRequest, "invalid since parameter", "https://ntfy.sh/docs/subscribe/api/#fetch-cached-messages"}
errHTTPBadRequestTopicInvalid = &errHTTP{40009, http.StatusBadRequest, "invalid topic: topic invalid", ""} errHTTPBadRequestTopicInvalid = &errHTTP{40009, http.StatusBadRequest, "invalid request: topic invalid", ""}
errHTTPBadRequestTopicDisallowed = &errHTTP{40010, http.StatusBadRequest, "invalid topic: topic name is disallowed", ""} errHTTPBadRequestTopicDisallowed = &errHTTP{40010, http.StatusBadRequest, "invalid request: topic name is disallowed", ""}
errHTTPBadRequestMessageNotUTF8 = &errHTTP{40011, http.StatusBadRequest, "invalid message: message must be UTF-8 encoded", ""} errHTTPBadRequestMessageNotUTF8 = &errHTTP{40011, http.StatusBadRequest, "invalid message: message must be UTF-8 encoded", ""}
errHTTPBadRequestAttachmentURLInvalid = &errHTTP{40013, http.StatusBadRequest, "invalid request: attachment URL is invalid", "https://ntfy.sh/docs/publish/#attachments"} errHTTPBadRequestAttachmentURLInvalid = &errHTTP{40013, http.StatusBadRequest, "invalid request: attachment URL is invalid", "https://ntfy.sh/docs/publish/#attachments"}
errHTTPBadRequestAttachmentsDisallowed = &errHTTP{40014, http.StatusBadRequest, "invalid request: attachments not allowed", "https://ntfy.sh/docs/config/#attachments"} errHTTPBadRequestAttachmentsDisallowed = &errHTTP{40014, http.StatusBadRequest, "invalid request: attachments not allowed", "https://ntfy.sh/docs/config/#attachments"}
@ -56,6 +56,7 @@ var (
errHTTPBadRequestSignupNotEnabled = &errHTTP{40022, http.StatusBadRequest, "invalid request: signup not enabled", "https://ntfy.sh/docs/config"} 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", ""} errHTTPBadRequestNoTokenProvided = &errHTTP{40023, http.StatusBadRequest, "invalid request: no token provided", ""}
errHTTPBadRequestJSONInvalid = &errHTTP{40024, http.StatusBadRequest, "invalid request: request body must be valid JSON", ""} errHTTPBadRequestJSONInvalid = &errHTTP{40024, http.StatusBadRequest, "invalid request: request body must be valid JSON", ""}
errHTTPBadRequestPermissionInvalid = &errHTTP{40025, http.StatusBadRequest, "invalid request: incorrect permission string", ""}
errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", ""} errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", ""}
errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication"} errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication"}
errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication"} errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication"}

View file

@ -165,7 +165,7 @@ func New(conf *Config) (*Server, error) {
} }
var userManager *user.Manager var userManager *user.Manager
if conf.AuthFile != "" { if conf.AuthFile != "" {
userManager, err = user.NewManager(conf.AuthFile, conf.AuthDefaultRead, conf.AuthDefaultWrite) userManager, err = user.NewManager(conf.AuthFile, conf.AuthDefault)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -101,19 +101,9 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, _ *http.Request, v *vis
if len(reservations) > 0 { if len(reservations) > 0 {
response.Reservations = make([]*apiAccountReservation, 0) response.Reservations = make([]*apiAccountReservation, 0)
for _, r := range reservations { for _, r := range reservations {
var everyone string
if r.AllowEveryoneRead && r.AllowEveryoneWrite {
everyone = "read-write"
} else if r.AllowEveryoneRead && !r.AllowEveryoneWrite {
everyone = "read-only"
} else if !r.AllowEveryoneRead && r.AllowEveryoneWrite {
everyone = "write-only"
} else {
everyone = "deny-all"
}
response.Reservations = append(response.Reservations, &apiAccountReservation{ response.Reservations = append(response.Reservations, &apiAccountReservation{
Topic: r.TopicPattern, Topic: r.Topic,
Everyone: everyone, Everyone: r.Everyone.String(),
}) })
} }
} }
@ -345,12 +335,14 @@ func (s *Server) handleAccountAccessAdd(w http.ResponseWriter, r *http.Request,
return errHTTPConflictTopicReserved return errHTTPConflictTopicReserved
} }
owner, username := v.user.Name, v.user.Name owner, username := v.user.Name, v.user.Name
everyoneRead := util.Contains([]string{"read-write", "rw", "read-only", "read", "ro"}, req.Everyone) everyone, err := user.ParsePermission(req.Everyone)
everyoneWrite := util.Contains([]string{"read-write", "rw", "write-only", "write", "wo"}, req.Everyone) if err != nil {
return errHTTPBadRequestPermissionInvalid
}
if err := s.userManager.AllowAccess(owner, username, req.Topic, true, true); err != nil { if err := s.userManager.AllowAccess(owner, username, req.Topic, true, true); err != nil {
return err return err
} }
if err := s.userManager.AllowAccess(owner, user.Everyone, req.Topic, everyoneRead, everyoneWrite); err != nil { if err := s.userManager.AllowAccess(owner, user.Everyone, req.Topic, everyone.IsRead(), everyone.IsWrite()); err != nil {
return err return err
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
@ -373,7 +365,7 @@ func (s *Server) handleAccountAccessDelete(w http.ResponseWriter, r *http.Reques
} }
authorized := false authorized := false
for _, r := range reservations { for _, r := range reservations {
if r.TopicPattern == topic { if r.Topic == topic {
authorized = true authorized = true
break break
} }

View file

@ -638,8 +638,7 @@ func TestServer_Auth_Success_Admin(t *testing.T) {
func TestServer_Auth_Success_User(t *testing.T) { func TestServer_Auth_Success_User(t *testing.T) {
c := newTestConfig(t) c := newTestConfig(t)
c.AuthFile = filepath.Join(t.TempDir(), "user.db") c.AuthFile = filepath.Join(t.TempDir(), "user.db")
c.AuthDefaultRead = false c.AuthDefault = user.PermissionDenyAll
c.AuthDefaultWrite = false
s := newTestServer(t, c) s := newTestServer(t, c)
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
@ -654,8 +653,7 @@ func TestServer_Auth_Success_User(t *testing.T) {
func TestServer_Auth_Success_User_MultipleTopics(t *testing.T) { func TestServer_Auth_Success_User_MultipleTopics(t *testing.T) {
c := newTestConfig(t) c := newTestConfig(t)
c.AuthFile = filepath.Join(t.TempDir(), "user.db") c.AuthFile = filepath.Join(t.TempDir(), "user.db")
c.AuthDefaultRead = false c.AuthDefault = user.PermissionDenyAll
c.AuthDefaultWrite = false
s := newTestServer(t, c) s := newTestServer(t, c)
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
@ -676,8 +674,7 @@ func TestServer_Auth_Success_User_MultipleTopics(t *testing.T) {
func TestServer_Auth_Fail_InvalidPass(t *testing.T) { func TestServer_Auth_Fail_InvalidPass(t *testing.T) {
c := newTestConfig(t) c := newTestConfig(t)
c.AuthFile = filepath.Join(t.TempDir(), "user.db") c.AuthFile = filepath.Join(t.TempDir(), "user.db")
c.AuthDefaultRead = false c.AuthDefault = user.PermissionDenyAll
c.AuthDefaultWrite = false
s := newTestServer(t, c) s := newTestServer(t, c)
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
@ -691,8 +688,7 @@ func TestServer_Auth_Fail_InvalidPass(t *testing.T) {
func TestServer_Auth_Fail_Unauthorized(t *testing.T) { func TestServer_Auth_Fail_Unauthorized(t *testing.T) {
c := newTestConfig(t) c := newTestConfig(t)
c.AuthFile = filepath.Join(t.TempDir(), "user.db") c.AuthFile = filepath.Join(t.TempDir(), "user.db")
c.AuthDefaultRead = false c.AuthDefault = user.PermissionDenyAll
c.AuthDefaultWrite = false
s := newTestServer(t, c) s := newTestServer(t, c)
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser)) require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
@ -707,8 +703,7 @@ func TestServer_Auth_Fail_Unauthorized(t *testing.T) {
func TestServer_Auth_Fail_CannotPublish(t *testing.T) { func TestServer_Auth_Fail_CannotPublish(t *testing.T) {
c := newTestConfig(t) c := newTestConfig(t)
c.AuthFile = filepath.Join(t.TempDir(), "user.db") c.AuthFile = filepath.Join(t.TempDir(), "user.db")
c.AuthDefaultRead = true // Open by default c.AuthDefault = user.PermissionReadWrite // Open by default
c.AuthDefaultWrite = true // Open by default
s := newTestServer(t, c) s := newTestServer(t, c)
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin)) require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
@ -739,8 +734,7 @@ func TestServer_Auth_Fail_CannotPublish(t *testing.T) {
func TestServer_Auth_ViaQuery(t *testing.T) { func TestServer_Auth_ViaQuery(t *testing.T) {
c := newTestConfig(t) c := newTestConfig(t)
c.AuthFile = filepath.Join(t.TempDir(), "user.db") c.AuthFile = filepath.Join(t.TempDir(), "user.db")
c.AuthDefaultRead = false c.AuthDefault = user.PermissionDenyAll
c.AuthDefaultWrite = false
s := newTestServer(t, c) s := newTestServer(t, c)
require.Nil(t, s.userManager.AddUser("ben", "some pass", user.RoleAdmin)) require.Nil(t, s.userManager.AddUser("ben", "some pass", user.RoleAdmin))

View file

@ -182,8 +182,7 @@ const (
// in a SQLite database. // in a SQLite database.
type Manager struct { type Manager struct {
db *sql.DB db *sql.DB
defaultRead bool // Default read permission if no ACL matches defaultAccess Permission // Default permission if no ACL matches
defaultWrite bool // Default write permission if no ACL matches
statsQueue map[string]*User // Username -> User, for "unimportant" user updates statsQueue map[string]*User // Username -> User, for "unimportant" user updates
tokenExpiryInterval time.Duration // Duration after which tokens expire, and by which tokens are extended tokenExpiryInterval time.Duration // Duration after which tokens expire, and by which tokens are extended
mu sync.Mutex mu sync.Mutex
@ -192,12 +191,12 @@ type Manager struct {
var _ Auther = (*Manager)(nil) var _ Auther = (*Manager)(nil)
// NewManager creates a new Manager instance // NewManager creates a new Manager instance
func NewManager(filename string, defaultRead, defaultWrite bool) (*Manager, error) { func NewManager(filename string, defaultAccess Permission) (*Manager, error) {
return newManager(filename, defaultRead, defaultWrite, userTokenExpiryDuration, userStatsQueueWriterInterval) return newManager(filename, defaultAccess, userTokenExpiryDuration, userStatsQueueWriterInterval)
} }
// NewManager creates a new Manager instance // NewManager creates a new Manager instance
func newManager(filename string, defaultRead, defaultWrite bool, tokenExpiryDuration, statsWriterInterval time.Duration) (*Manager, error) { func newManager(filename string, defaultAccess Permission, tokenExpiryDuration, statsWriterInterval time.Duration) (*Manager, error) {
db, err := sql.Open("sqlite3", filename) db, err := sql.Open("sqlite3", filename)
if err != nil { if err != nil {
return nil, err return nil, err
@ -207,8 +206,7 @@ func newManager(filename string, defaultRead, defaultWrite bool, tokenExpiryDura
} }
manager := &Manager{ manager := &Manager{
db: db, db: db,
defaultRead: defaultRead, defaultAccess: defaultAccess,
defaultWrite: defaultWrite,
statsQueue: make(map[string]*User), statsQueue: make(map[string]*User),
tokenExpiryInterval: tokenExpiryDuration, tokenExpiryInterval: tokenExpiryDuration,
} }
@ -368,7 +366,7 @@ func (a *Manager) Authorize(user *User, topic string, perm Permission) error {
} }
defer rows.Close() defer rows.Close()
if !rows.Next() { if !rows.Next() {
return a.resolvePerms(a.defaultRead, a.defaultWrite, perm) return a.resolvePerms(a.defaultAccess, perm)
} }
var read, write bool var read, write bool
if err := rows.Scan(&read, &write); err != nil { if err := rows.Scan(&read, &write); err != nil {
@ -376,13 +374,13 @@ func (a *Manager) Authorize(user *User, topic string, perm Permission) error {
} else if err := rows.Err(); err != nil { } else if err := rows.Err(); err != nil {
return err return err
} }
return a.resolvePerms(read, write, perm) return a.resolvePerms(NewPermission(read, write), perm)
} }
func (a *Manager) resolvePerms(read, write bool, perm Permission) error { func (a *Manager) resolvePerms(base, perm Permission) error {
if perm == PermissionRead && read { if perm == PermissionRead && base.IsRead() {
return nil return nil
} else if perm == PermissionWrite && write { } else if perm == PermissionWrite && base.IsWrite() {
return nil return nil
} }
return ErrUnauthorized return ErrUnauthorized
@ -534,8 +532,7 @@ func (a *Manager) Grants(username string) ([]Grant, error) {
} }
grants = append(grants, Grant{ grants = append(grants, Grant{
TopicPattern: fromSQLWildcard(topic), TopicPattern: fromSQLWildcard(topic),
AllowRead: read, Allow: NewPermission(read, write),
AllowWrite: write,
}) })
} }
return grants, nil return grants, nil
@ -551,19 +548,17 @@ func (a *Manager) Reservations(username string) ([]Reservation, error) {
reservations := make([]Reservation, 0) reservations := make([]Reservation, 0)
for rows.Next() { for rows.Next() {
var topic string var topic string
var read, write bool var ownerRead, ownerWrite bool
var everyoneRead, everyoneWrite sql.NullBool var everyoneRead, everyoneWrite sql.NullBool
if err := rows.Scan(&topic, &read, &write, &everyoneRead, &everyoneWrite); err != nil { if err := rows.Scan(&topic, &ownerRead, &ownerWrite, &everyoneRead, &everyoneWrite); err != nil {
return nil, err return nil, err
} else if err := rows.Err(); err != nil { } else if err := rows.Err(); err != nil {
return nil, err return nil, err
} }
reservations = append(reservations, Reservation{ reservations = append(reservations, Reservation{
TopicPattern: topic, Topic: topic,
AllowRead: read, Owner: NewPermission(ownerRead, ownerWrite),
AllowWrite: write, Everyone: NewPermission(everyoneRead.Bool, everyoneWrite.Bool), // false if null
AllowEveryoneRead: everyoneRead.Bool, // false if null
AllowEveryoneWrite: everyoneWrite.Bool, // false if null
}) })
} }
return reservations, nil return reservations, nil
@ -659,8 +654,8 @@ func (a *Manager) ResetAccess(username string, topicPattern string) error {
} }
// DefaultAccess returns the default read/write access if no access control entry matches // DefaultAccess returns the default read/write access if no access control entry matches
func (a *Manager) DefaultAccess() (read bool, write bool) { func (a *Manager) DefaultAccess() Permission {
return a.defaultRead, a.defaultWrite return a.defaultAccess
} }
func toSQLWildcard(s string) string { func toSQLWildcard(s string) string {

View file

@ -12,7 +12,7 @@ import (
const minBcryptTimingMillis = int64(50) // Ideally should be >100ms, but this should also run on a Raspberry Pi without massive resources const minBcryptTimingMillis = int64(50) // Ideally should be >100ms, but this should also run on a Raspberry Pi without massive resources
func TestManager_FullScenario_Default_DenyAll(t *testing.T) { func TestManager_FullScenario_Default_DenyAll(t *testing.T) {
a := newTestManager(t, false, false) a := newTestManager(t, PermissionDenyAll)
require.Nil(t, a.AddUser("phil", "phil", RoleAdmin)) require.Nil(t, a.AddUser("phil", "phil", RoleAdmin))
require.Nil(t, a.AddUser("ben", "ben", RoleUser)) require.Nil(t, a.AddUser("ben", "ben", RoleUser))
require.Nil(t, a.AllowAccess("", "ben", "mytopic", true, true)) require.Nil(t, a.AllowAccess("", "ben", "mytopic", true, true))
@ -28,19 +28,25 @@ func TestManager_FullScenario_Default_DenyAll(t *testing.T) {
require.Equal(t, "phil", phil.Name) require.Equal(t, "phil", phil.Name)
require.True(t, strings.HasPrefix(phil.Hash, "$2a$10$")) require.True(t, strings.HasPrefix(phil.Hash, "$2a$10$"))
require.Equal(t, RoleAdmin, phil.Role) require.Equal(t, RoleAdmin, phil.Role)
require.Equal(t, []Grant{}, phil.Grants)
philGrants, err := a.Grants("phil")
require.Nil(t, err)
require.Equal(t, []Grant{}, philGrants)
ben, err := a.Authenticate("ben", "ben") ben, err := a.Authenticate("ben", "ben")
require.Nil(t, err) require.Nil(t, err)
require.Equal(t, "ben", ben.Name) require.Equal(t, "ben", ben.Name)
require.True(t, strings.HasPrefix(ben.Hash, "$2a$10$")) require.True(t, strings.HasPrefix(ben.Hash, "$2a$10$"))
require.Equal(t, RoleUser, ben.Role) require.Equal(t, RoleUser, ben.Role)
benGrants, err := a.Grants("ben")
require.Nil(t, err)
require.Equal(t, []Grant{ require.Equal(t, []Grant{
{"mytopic", true, true, false}, {"mytopic", PermissionReadWrite},
{"writeme", false, true, false}, {"writeme", PermissionWrite},
{"readme", true, false, false}, {"readme", PermissionRead},
{"everyonewrite", false, false, false}, {"everyonewrite", PermissionDenyAll},
}, ben.Grants) }, benGrants)
notben, err := a.Authenticate("ben", "this is wrong") notben, err := a.Authenticate("ben", "this is wrong")
require.Nil(t, notben) require.Nil(t, notben)
@ -85,20 +91,20 @@ func TestManager_FullScenario_Default_DenyAll(t *testing.T) {
} }
func TestManager_AddUser_Invalid(t *testing.T) { func TestManager_AddUser_Invalid(t *testing.T) {
a := newTestManager(t, false, false) a := newTestManager(t, PermissionDenyAll)
require.Equal(t, ErrInvalidArgument, a.AddUser(" invalid ", "pass", RoleAdmin)) require.Equal(t, ErrInvalidArgument, a.AddUser(" invalid ", "pass", RoleAdmin))
require.Equal(t, ErrInvalidArgument, a.AddUser("validuser", "pass", "invalid-role")) require.Equal(t, ErrInvalidArgument, a.AddUser("validuser", "pass", "invalid-role"))
} }
func TestManager_AddUser_Timing(t *testing.T) { func TestManager_AddUser_Timing(t *testing.T) {
a := newTestManager(t, false, false) a := newTestManager(t, PermissionDenyAll)
start := time.Now().UnixMilli() start := time.Now().UnixMilli()
require.Nil(t, a.AddUser("user", "pass", RoleAdmin)) require.Nil(t, a.AddUser("user", "pass", RoleAdmin))
require.GreaterOrEqual(t, time.Now().UnixMilli()-start, minBcryptTimingMillis) require.GreaterOrEqual(t, time.Now().UnixMilli()-start, minBcryptTimingMillis)
} }
func TestManager_Authenticate_Timing(t *testing.T) { func TestManager_Authenticate_Timing(t *testing.T) {
a := newTestManager(t, false, false) a := newTestManager(t, PermissionDenyAll)
require.Nil(t, a.AddUser("user", "pass", RoleAdmin)) require.Nil(t, a.AddUser("user", "pass", RoleAdmin))
// Timing a correct attempt // Timing a correct attempt
@ -121,7 +127,7 @@ func TestManager_Authenticate_Timing(t *testing.T) {
} }
func TestManager_UserManagement(t *testing.T) { func TestManager_UserManagement(t *testing.T) {
a := newTestManager(t, false, false) a := newTestManager(t, PermissionDenyAll)
require.Nil(t, a.AddUser("phil", "phil", RoleAdmin)) require.Nil(t, a.AddUser("phil", "phil", RoleAdmin))
require.Nil(t, a.AddUser("ben", "ben", RoleUser)) require.Nil(t, a.AddUser("ben", "ben", RoleUser))
require.Nil(t, a.AllowAccess("", "ben", "mytopic", true, true)) require.Nil(t, a.AllowAccess("", "ben", "mytopic", true, true))
@ -137,29 +143,38 @@ func TestManager_UserManagement(t *testing.T) {
require.Equal(t, "phil", phil.Name) require.Equal(t, "phil", phil.Name)
require.True(t, strings.HasPrefix(phil.Hash, "$2a$10$")) require.True(t, strings.HasPrefix(phil.Hash, "$2a$10$"))
require.Equal(t, RoleAdmin, phil.Role) require.Equal(t, RoleAdmin, phil.Role)
require.Equal(t, []Grant{}, phil.Grants)
philGrants, err := a.Grants("phil")
require.Nil(t, err)
require.Equal(t, []Grant{}, philGrants)
ben, err := a.User("ben") ben, err := a.User("ben")
require.Nil(t, err) require.Nil(t, err)
require.Equal(t, "ben", ben.Name) require.Equal(t, "ben", ben.Name)
require.True(t, strings.HasPrefix(ben.Hash, "$2a$10$")) require.True(t, strings.HasPrefix(ben.Hash, "$2a$10$"))
require.Equal(t, RoleUser, ben.Role) require.Equal(t, RoleUser, ben.Role)
benGrants, err := a.Grants("ben")
require.Nil(t, err)
require.Equal(t, []Grant{ require.Equal(t, []Grant{
{"mytopic", true, true, false}, {"mytopic", PermissionReadWrite},
{"writeme", false, true, false}, {"writeme", PermissionWrite},
{"readme", true, false, false}, {"readme", PermissionRead},
{"everyonewrite", false, false, false}, {"everyonewrite", PermissionDenyAll},
}, ben.Grants) }, benGrants)
everyone, err := a.User(Everyone) everyone, err := a.User(Everyone)
require.Nil(t, err) require.Nil(t, err)
require.Equal(t, "*", everyone.Name) require.Equal(t, "*", everyone.Name)
require.Equal(t, "", everyone.Hash) require.Equal(t, "", everyone.Hash)
require.Equal(t, RoleAnonymous, everyone.Role) require.Equal(t, RoleAnonymous, everyone.Role)
everyoneGrants, err := a.Grants(Everyone)
require.Nil(t, err)
require.Equal(t, []Grant{ require.Equal(t, []Grant{
{"everyonewrite", true, true, false}, {"everyonewrite", PermissionReadWrite},
{"announcements", true, false, false}, {"announcements", PermissionRead},
}, everyone.Grants) }, everyoneGrants)
// Ben: Before revoking // Ben: Before revoking
require.Nil(t, a.AllowAccess("", "ben", "mytopic", true, true)) // Overwrite! require.Nil(t, a.AllowAccess("", "ben", "mytopic", true, true)) // Overwrite!
@ -203,7 +218,7 @@ func TestManager_UserManagement(t *testing.T) {
} }
func TestManager_ChangePassword(t *testing.T) { func TestManager_ChangePassword(t *testing.T) {
a := newTestManager(t, false, false) a := newTestManager(t, PermissionDenyAll)
require.Nil(t, a.AddUser("phil", "phil", RoleAdmin)) require.Nil(t, a.AddUser("phil", "phil", RoleAdmin))
_, err := a.Authenticate("phil", "phil") _, err := a.Authenticate("phil", "phil")
@ -217,7 +232,7 @@ func TestManager_ChangePassword(t *testing.T) {
} }
func TestManager_ChangeRole(t *testing.T) { func TestManager_ChangeRole(t *testing.T) {
a := newTestManager(t, false, false) a := newTestManager(t, PermissionDenyAll)
require.Nil(t, a.AddUser("ben", "ben", RoleUser)) require.Nil(t, a.AddUser("ben", "ben", RoleUser))
require.Nil(t, a.AllowAccess("", "ben", "mytopic", 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", "readme", true, false))
@ -225,18 +240,24 @@ func TestManager_ChangeRole(t *testing.T) {
ben, err := a.User("ben") ben, err := a.User("ben")
require.Nil(t, err) require.Nil(t, err)
require.Equal(t, RoleUser, ben.Role) require.Equal(t, RoleUser, ben.Role)
require.Equal(t, 2, len(ben.Grants))
benGrants, err := a.Grants("ben")
require.Nil(t, err)
require.Equal(t, 2, len(benGrants))
require.Nil(t, a.ChangeRole("ben", RoleAdmin)) require.Nil(t, a.ChangeRole("ben", RoleAdmin))
ben, err = a.User("ben") ben, err = a.User("ben")
require.Nil(t, err) require.Nil(t, err)
require.Equal(t, RoleAdmin, ben.Role) require.Equal(t, RoleAdmin, ben.Role)
require.Equal(t, 0, len(ben.Grants))
benGrants, err = a.Grants("ben")
require.Nil(t, err)
require.Equal(t, 0, len(benGrants))
} }
func TestManager_Token_Valid(t *testing.T) { func TestManager_Token_Valid(t *testing.T) {
a := newTestManager(t, false, false) a := newTestManager(t, PermissionDenyAll)
require.Nil(t, a.AddUser("ben", "ben", RoleUser)) require.Nil(t, a.AddUser("ben", "ben", RoleUser))
u, err := a.User("ben") u, err := a.User("ben")
@ -261,7 +282,7 @@ func TestManager_Token_Valid(t *testing.T) {
} }
func TestManager_Token_Invalid(t *testing.T) { func TestManager_Token_Invalid(t *testing.T) {
a := newTestManager(t, false, false) a := newTestManager(t, PermissionDenyAll)
require.Nil(t, a.AddUser("ben", "ben", RoleUser)) require.Nil(t, a.AddUser("ben", "ben", RoleUser))
u, err := a.AuthenticateToken(strings.Repeat("x", 32)) // 32 == token length u, err := a.AuthenticateToken(strings.Repeat("x", 32)) // 32 == token length
@ -274,7 +295,7 @@ func TestManager_Token_Invalid(t *testing.T) {
} }
func TestManager_Token_Expire(t *testing.T) { func TestManager_Token_Expire(t *testing.T) {
a := newTestManager(t, false, false) a := newTestManager(t, PermissionDenyAll)
require.Nil(t, a.AddUser("ben", "ben", RoleUser)) require.Nil(t, a.AddUser("ben", "ben", RoleUser))
u, err := a.User("ben") u, err := a.User("ben")
@ -322,7 +343,7 @@ func TestManager_Token_Expire(t *testing.T) {
} }
func TestManager_Token_Extend(t *testing.T) { func TestManager_Token_Extend(t *testing.T) {
a := newTestManager(t, false, false) a := newTestManager(t, PermissionDenyAll)
require.Nil(t, a.AddUser("ben", "ben", RoleUser)) require.Nil(t, a.AddUser("ben", "ben", RoleUser))
// Try to extend token for user without token // Try to extend token for user without token
@ -349,7 +370,7 @@ func TestManager_Token_Extend(t *testing.T) {
} }
func TestManager_EnqueueStats(t *testing.T) { func TestManager_EnqueueStats(t *testing.T) {
a, err := newManager(filepath.Join(t.TempDir(), "db"), true, true, time.Hour, 1500*time.Millisecond) a, err := newManager(filepath.Join(t.TempDir(), "db"), PermissionReadWrite, time.Hour, 1500*time.Millisecond)
require.Nil(t, err) require.Nil(t, err)
require.Nil(t, a.AddUser("ben", "ben", RoleUser)) require.Nil(t, a.AddUser("ben", "ben", RoleUser))
@ -379,7 +400,7 @@ func TestManager_EnqueueStats(t *testing.T) {
} }
func TestManager_ChangeSettings(t *testing.T) { func TestManager_ChangeSettings(t *testing.T) {
a, err := newManager(filepath.Join(t.TempDir(), "db"), true, true, time.Hour, 1500*time.Millisecond) a, err := newManager(filepath.Join(t.TempDir(), "db"), PermissionReadWrite, time.Hour, 1500*time.Millisecond)
require.Nil(t, err) require.Nil(t, err)
require.Nil(t, a.AddUser("ben", "ben", RoleUser)) require.Nil(t, a.AddUser("ben", "ben", RoleUser))
@ -461,7 +482,7 @@ func TestSqliteCache_Migration_From1(t *testing.T) {
require.Nil(t, err) require.Nil(t, err)
// Create manager to trigger migration // Create manager to trigger migration
a := newTestManagerFromFile(t, filename, false, false, userTokenExpiryDuration, userStatsQueueWriterInterval) a := newTestManagerFromFile(t, filename, PermissionDenyAll, userTokenExpiryDuration, userStatsQueueWriterInterval)
checkSchemaVersion(t, a.db) checkSchemaVersion(t, a.db)
users, err := a.Users() users, err := a.Users()
@ -469,26 +490,32 @@ func TestSqliteCache_Migration_From1(t *testing.T) {
require.Equal(t, 3, len(users)) require.Equal(t, 3, len(users))
phil, ben, everyone := users[0], users[1], users[2] phil, ben, everyone := users[0], users[1], users[2]
philGrants, err := a.Grants("phil")
require.Nil(t, err)
benGrants, err := a.Grants("ben")
require.Nil(t, err)
everyoneGrants, err := a.Grants(Everyone)
require.Nil(t, err)
require.Equal(t, "phil", phil.Name) require.Equal(t, "phil", phil.Name)
require.Equal(t, RoleAdmin, phil.Role) require.Equal(t, RoleAdmin, phil.Role)
require.Equal(t, 0, len(phil.Grants)) require.Equal(t, 0, len(philGrants))
require.Equal(t, "ben", ben.Name) require.Equal(t, "ben", ben.Name)
require.Equal(t, RoleUser, ben.Role) require.Equal(t, RoleUser, ben.Role)
require.Equal(t, 2, len(ben.Grants)) require.Equal(t, 2, len(benGrants))
require.Equal(t, "stats", ben.Grants[0].TopicPattern) require.Equal(t, "stats", benGrants[0].TopicPattern)
require.Equal(t, true, ben.Grants[0].AllowRead) require.Equal(t, PermissionReadWrite, benGrants[0].Allow)
require.Equal(t, true, ben.Grants[0].AllowWrite) require.Equal(t, "secret", benGrants[1].TopicPattern)
require.Equal(t, "secret", ben.Grants[1].TopicPattern) require.Equal(t, PermissionRead, benGrants[1].Allow)
require.Equal(t, true, ben.Grants[1].AllowRead)
require.Equal(t, false, ben.Grants[1].AllowWrite)
require.Equal(t, Everyone, everyone.Name) require.Equal(t, Everyone, everyone.Name)
require.Equal(t, RoleAnonymous, everyone.Role) require.Equal(t, RoleAnonymous, everyone.Role)
require.Equal(t, 1, len(everyone.Grants)) require.Equal(t, 1, len(everyoneGrants))
require.Equal(t, "stats", everyone.Grants[0].TopicPattern) require.Equal(t, "stats", everyoneGrants[0].TopicPattern)
require.Equal(t, true, everyone.Grants[0].AllowRead) require.Equal(t, PermissionRead, everyoneGrants[0].Allow)
require.Equal(t, false, everyone.Grants[0].AllowWrite)
} }
func checkSchemaVersion(t *testing.T, db *sql.DB) { func checkSchemaVersion(t *testing.T, db *sql.DB) {
@ -502,12 +529,12 @@ func checkSchemaVersion(t *testing.T, db *sql.DB) {
require.Nil(t, rows.Close()) require.Nil(t, rows.Close())
} }
func newTestManager(t *testing.T, defaultRead, defaultWrite bool) *Manager { func newTestManager(t *testing.T, defaultAccess Permission) *Manager {
return newTestManagerFromFile(t, filepath.Join(t.TempDir(), "user.db"), defaultRead, defaultWrite, userTokenExpiryDuration, userStatsQueueWriterInterval) return newTestManagerFromFile(t, filepath.Join(t.TempDir(), "user.db"), defaultAccess, userTokenExpiryDuration, userStatsQueueWriterInterval)
} }
func newTestManagerFromFile(t *testing.T, filename string, defaultRead, defaultWrite bool, tokenExpiryDuration, statsWriterInterval time.Duration) *Manager { func newTestManagerFromFile(t *testing.T, filename string, defaultAccess Permission, tokenExpiryDuration, statsWriterInterval time.Duration) *Manager {
a, err := newManager(filename, defaultRead, defaultWrite, tokenExpiryDuration, statsWriterInterval) a, err := newManager(filename, defaultAccess, tokenExpiryDuration, statsWriterInterval)
require.Nil(t, err) require.Nil(t, err)
return a return a
} }

View file

@ -85,31 +85,79 @@ type Stats struct {
Emails int64 Emails int64
} }
// Grant is a struct that represents an access control entry to a topic // Grant is a struct that represents an access control entry to a topic by a user
type Grant struct { type Grant struct {
TopicPattern string // May include wildcard (*) TopicPattern string // May include wildcard (*)
AllowRead bool Allow Permission
AllowWrite bool
} }
// Reservation is a struct that represents the ownership over a topic by a user // Reservation is a struct that represents the ownership over a topic by a user
type Reservation struct { type Reservation struct {
TopicPattern string Topic string
AllowRead bool Owner Permission
AllowWrite bool Everyone Permission
AllowEveryoneRead bool
AllowEveryoneWrite bool
} }
// Permission represents a read or write permission to a topic // Permission represents a read or write permission to a topic
type Permission int type Permission uint8
// Permissions to a topic // Permissions to a topic
const ( const (
PermissionRead = Permission(1) PermissionDenyAll Permission = iota
PermissionWrite = Permission(2) PermissionRead
PermissionWrite
PermissionReadWrite // 3!
) )
func NewPermission(read, write bool) Permission {
p := uint8(0)
if read {
p |= uint8(PermissionRead)
}
if write {
p |= uint8(PermissionWrite)
}
return Permission(p)
}
func ParsePermission(s string) (Permission, error) {
switch s {
case "read-write", "rw":
return NewPermission(true, true), nil
case "read-only", "read", "ro":
return NewPermission(true, false), nil
case "write-only", "write", "wo":
return NewPermission(false, true), nil
case "deny-all", "deny", "none":
return NewPermission(false, false), nil
default:
return NewPermission(false, false), errors.New("invalid permission")
}
}
func (p Permission) IsRead() bool {
return p&PermissionRead != 0
}
func (p Permission) IsWrite() bool {
return p&PermissionWrite != 0
}
func (p Permission) IsReadWrite() bool {
return p.IsRead() && p.IsWrite()
}
func (p Permission) String() string {
if p.IsReadWrite() {
return "read-write"
} else if p.IsRead() {
return "read-only"
} else if p.IsWrite() {
return "write-only"
}
return "deny-all"
}
// Role represents a user's role, either admin or regular user // Role represents a user's role, either admin or regular user
type Role string type Role string